diff --git a/examples/http_api/.env.local b/examples/http_api/.env.local new file mode 100644 index 0000000..f65ecec --- /dev/null +++ b/examples/http_api/.env.local @@ -0,0 +1,31 @@ +#App Configs +RUST_ENV=local +LOG_LEVEL=debug +SECRET_MANAGER=NONE + +HOST_NAME=0.0.0.0 +APP_PORT=4444 + +#Identity Server +IDENTITY_SERVER_URL= +IDENTITY_SERVER_REALM= +IDENTITY_SERVER_AUDIENCE= +IDENTITY_SERVER_ISSUER= +IDENTITY_SERVER_GRANT_TYPE= +IDENTITY_SERVER_CLIENT_ID= +IDENTITY_SERVER_CLIENT_SECRET= + +#OTLP Configs +ENABLE_TRACES=false +TRACE_SERVICE_TYPE=HTTP +TRACE_HOST=localhost +TRACE_KEY=key +TRACE_EXPORT_TIMEOUT=60 +TRACE_EXPORT_RATE_BASE=0.7 + +ENABLE_METRICS=false +METRIC_SERVICE_TYPE=HTTP +METRIC_HOST=localhost +METRIC_KEY=key +METRIC_EXPORT_TIMEOUT=60 +METRIC_EXPORT_RATE_BASE=0.7 \ No newline at end of file diff --git a/examples/http_api/.env.prod b/examples/http_api/.env.prod new file mode 100644 index 0000000..f65ecec --- /dev/null +++ b/examples/http_api/.env.prod @@ -0,0 +1,31 @@ +#App Configs +RUST_ENV=local +LOG_LEVEL=debug +SECRET_MANAGER=NONE + +HOST_NAME=0.0.0.0 +APP_PORT=4444 + +#Identity Server +IDENTITY_SERVER_URL= +IDENTITY_SERVER_REALM= +IDENTITY_SERVER_AUDIENCE= +IDENTITY_SERVER_ISSUER= +IDENTITY_SERVER_GRANT_TYPE= +IDENTITY_SERVER_CLIENT_ID= +IDENTITY_SERVER_CLIENT_SECRET= + +#OTLP Configs +ENABLE_TRACES=false +TRACE_SERVICE_TYPE=HTTP +TRACE_HOST=localhost +TRACE_KEY=key +TRACE_EXPORT_TIMEOUT=60 +TRACE_EXPORT_RATE_BASE=0.7 + +ENABLE_METRICS=false +METRIC_SERVICE_TYPE=HTTP +METRIC_HOST=localhost +METRIC_KEY=key +METRIC_EXPORT_TIMEOUT=60 +METRIC_EXPORT_RATE_BASE=0.7 \ No newline at end of file diff --git a/examples/http_api/.env.staging b/examples/http_api/.env.staging new file mode 100644 index 0000000..f65ecec --- /dev/null +++ b/examples/http_api/.env.staging @@ -0,0 +1,31 @@ +#App Configs +RUST_ENV=local +LOG_LEVEL=debug +SECRET_MANAGER=NONE + +HOST_NAME=0.0.0.0 +APP_PORT=4444 + +#Identity Server +IDENTITY_SERVER_URL= +IDENTITY_SERVER_REALM= +IDENTITY_SERVER_AUDIENCE= +IDENTITY_SERVER_ISSUER= +IDENTITY_SERVER_GRANT_TYPE= +IDENTITY_SERVER_CLIENT_ID= +IDENTITY_SERVER_CLIENT_SECRET= + +#OTLP Configs +ENABLE_TRACES=false +TRACE_SERVICE_TYPE=HTTP +TRACE_HOST=localhost +TRACE_KEY=key +TRACE_EXPORT_TIMEOUT=60 +TRACE_EXPORT_RATE_BASE=0.7 + +ENABLE_METRICS=false +METRIC_SERVICE_TYPE=HTTP +METRIC_HOST=localhost +METRIC_KEY=key +METRIC_EXPORT_TIMEOUT=60 +METRIC_EXPORT_RATE_BASE=0.7 \ No newline at end of file diff --git a/examples/http_api/.gitignore b/examples/http_api/.gitignore new file mode 100644 index 0000000..1de5659 --- /dev/null +++ b/examples/http_api/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/examples/http_api/Cargo.toml b/examples/http_api/Cargo.toml new file mode 100644 index 0000000..ac0c395 --- /dev/null +++ b/examples/http_api/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "http_api" +version = "0.1.0" +edition = "2021" + +[dependencies] +configs = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0"} +configs-builder = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0"} +http-components = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0", features = ["auth"]} +http-server = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0", features = ["openapi"]} +auth = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0"} +logging = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0"} +traces = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0"} +metrics = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0"} +sql-pool = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0", features = ["postgres"] } +migrator = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0", features = ["postgres"] } +health-readiness = {git = "ssh://git@github.com/tointernet/ruskit.git", rev = "v0.3.0"} + +actix-web = { version = "4.5.1" } +serde = { version = "1.0.200" } +serde_json = { version = "1.0.116" } +opentelemetry = { version = "0.22" } +async-trait = { version = "0.1.77" } +tracing = { version = "0.1.40" } +tokio = { version = "1.37.0", features = ["default", "rt-multi-thread", "macros", "signal"]} +utoipa = { version = "4.2.0" } +validator = { version = "0.18.1" } \ No newline at end of file diff --git a/examples/http_api/README.md b/examples/http_api/README.md new file mode 100644 index 0000000..e567e33 --- /dev/null +++ b/examples/http_api/README.md @@ -0,0 +1,5 @@ +# HTTP API Example + +- Swagger +- Opentelemetry Trace with OTLP +- Opentelemetry Metrics with OTLP diff --git a/examples/http_api/src/main.rs b/examples/http_api/src/main.rs new file mode 100644 index 0000000..2ad424c --- /dev/null +++ b/examples/http_api/src/main.rs @@ -0,0 +1,52 @@ +mod openapi; +mod todos; + +use actix_web::web::{Data, ServiceConfig}; +use auth::{auth0::Auth0JwtManager, manager::JwtManager}; +use configs::{Configs, Empty}; +use configs_builder::ConfigBuilder; +use http_components::CustomServiceConfigure; +use http_server::server::HTTPServer; +use openapi::ApiDoc; +use std::{error::Error, sync::Arc}; +use utoipa::OpenApi; + +use todos::routes as todos_routes; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cfgs = default_setup().await?; + + let doc = ApiDoc::openapi(); + + let identity_configs = cfgs.identity.clone(); + + HTTPServer::new(&cfgs.app) + //if you need to share structs to all the controllers, use something similar + .custom_configure(CustomServiceConfigure::new( + move |cfg: &mut ServiceConfig| { + let auth0_manager: Arc = Auth0JwtManager::new(&identity_configs); + cfg.app_data(Data::::from(auth0_manager)); + }, + )) + .custom_configure(todos_routes::basic_routes()) + .openapi(&doc) + .start() + .await?; + + Ok(()) +} + +async fn default_setup<'cfg>() -> Result, Box> { + let cfg = ConfigBuilder::new() + .trace() + .metric() + .identity_server() + .build::() + .await?; + + traces::provider::init(&cfg)?; + metrics::provider::init(&cfg)?; + + Ok(cfg) +} diff --git a/examples/http_api/src/openapi.rs b/examples/http_api/src/openapi.rs new file mode 100644 index 0000000..6938e11 --- /dev/null +++ b/examples/http_api/src/openapi.rs @@ -0,0 +1,54 @@ +use crate::todos::{controllers as tc, viewmodels as tvm}; +use http_components::viewmodels::HTTPError; +use utoipa::{ + openapi::{ + self, + security::{Http, HttpAuthScheme, SecurityScheme}, + }, + Modify, OpenApi, +}; + +#[derive(OpenApi)] +#[openapi( + paths( + tc::post, tc::get, tc::list, tc::delete, + ), + components( + schemas( + HTTPError, + tvm::ToDoRequest, tvm::ToDoResponse, + ) + ), + tags( + (name = "examples", description = "Examples endpoints."), + ), + modifiers(&SecurityAddon), + info( + title = "Hedro HTTP API", + version = "v0.0.1", + description = "Hedro HTTP API's built in rust" + ), +)] +#[cfg_attr(debug_assertions, openapi( + servers( + (url = "http://localhost:4444", description = "Local server"), + ), +))] +#[cfg_attr(not(debug_assertions), openapi( + servers( + (url = "https://stg-api.com.br", description = "Staging server"), + ), +))] +pub struct ApiDoc; + +pub struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut openapi::OpenApi) { + let components = openapi.components.as_mut().unwrap(); + components.add_security_scheme( + "auth", + SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), + ) + } +} diff --git a/examples/http_api/src/todos/controllers/mod.rs b/examples/http_api/src/todos/controllers/mod.rs new file mode 100644 index 0000000..eb94764 --- /dev/null +++ b/examples/http_api/src/todos/controllers/mod.rs @@ -0,0 +1,3 @@ +mod todos; + +pub use todos::{__path_delete, __path_get, __path_list, __path_post, delete, get, list, post}; diff --git a/examples/http_api/src/todos/controllers/todos.rs b/examples/http_api/src/todos/controllers/todos.rs new file mode 100644 index 0000000..500011e --- /dev/null +++ b/examples/http_api/src/todos/controllers/todos.rs @@ -0,0 +1,103 @@ +use crate::todos::viewmodels::ToDoRequest; +use actix_web::{delete, get, post, web::Json, HttpRequest, HttpResponse, Responder}; +use http_components::extractors::JwtAuthenticateExtractor; + +/// Request to create a new ToDo. +/// +/// If the request was registered correctly this endpoint will return 201 Accepted and 4xx/5xx if some error occur. +/// +#[utoipa::path( + post, + path = "", + context_path = "/v1/todos", + tag = "todos", + request_body = ToDoRequest, + responses( + (status = 202, description = "Todo requested successfully", body = ToDoResponse), + (status = 400, description = "Bad request", body = HTTPError), + (status = 401, description = "Unauthorized", body = HTTPError), + (status = 403, description = "Forbidden", body = HTTPError), + (status = 500, description = "Internal error", body = HTTPError) + ), + security() +)] +#[post("")] +pub async fn post(_thing: Json, _auth: JwtAuthenticateExtractor) -> impl Responder { + HttpResponse::Ok().body("post::things") +} + +/// Request to get all ToDo's that was created. +/// +/// If the request was process correctly this endpoint will return 200 Ok and 4xx/5xx if some error occur. +/// +#[utoipa::path( + get, + path = "", + context_path = "/v1/todos", + tag = "todos", + responses( + (status = 200, description = "Success", body = Vec), + (status = 400, description = "Bad request", body = HTTPError), + (status = 401, description = "Unauthorized", body = HTTPError), + (status = 403, description = "Forbidden", body = HTTPError), + (status = 500, description = "Internal error", body = HTTPError) + ), + security( + ("auth" = []) + ) +)] +#[get("")] +pub async fn list(_req: HttpRequest, _: JwtAuthenticateExtractor) -> impl Responder { + HttpResponse::Ok().body("list::things") +} + +/// Request to get a specific ToDo by ID. +/// +/// If the request was process correctly this endpoint will return 200 Ok and 4xx/5xx if some error occur. +/// +#[utoipa::path( + get, + path = "/{id}", + context_path = "/v1/todos", + tag = "todos", + responses( + (status = 200, description = "Success", body = ToDoResponse), + (status = 400, description = "Bad request", body = HTTPError), + (status = 401, description = "Unauthorized", body = HTTPError), + (status = 403, description = "Forbidden", body = HTTPError), + (status = 500, description = "Internal error", body = HTTPError) + ), + security( + ("auth" = []) + ) +)] +#[get("/{id}")] +pub async fn get(_req: HttpRequest, _: JwtAuthenticateExtractor) -> impl Responder { + HttpResponse::Ok().body("get::things") +} + +/// Request to delete a specific ToDo by ID. +/// +/// If the request was process correctly this endpoint will return 200 Ok and 4xx/5xx if some error occur. +/// +#[utoipa::path( + delete, + path = "/{id}", + context_path = "/v1/todos", + tag = "todos", + request_body = ToDoRequest, + responses( + (status = 200, description = "Deleted", body = ToDoResponse), + (status = 400, description = "Bad request", body = HTTPError), + (status = 401, description = "Unauthorized", body = HTTPError), + (status = 403, description = "Forbidden", body = HTTPError), + (status = 500, description = "Internal error", body = HTTPError) + ), + security( + ("auth" = []) + ) +)] +#[delete("/{id}")] +pub async fn delete(_req: HttpRequest, _: JwtAuthenticateExtractor) -> impl Responder { + HttpResponse::Ok().body("delete::things") +} diff --git a/examples/http_api/src/todos/mod.rs b/examples/http_api/src/todos/mod.rs new file mode 100644 index 0000000..0593fcc --- /dev/null +++ b/examples/http_api/src/todos/mod.rs @@ -0,0 +1,3 @@ +pub mod controllers; +pub mod routes; +pub mod viewmodels; diff --git a/examples/http_api/src/todos/routes/mod.rs b/examples/http_api/src/todos/routes/mod.rs new file mode 100644 index 0000000..7ebcf17 --- /dev/null +++ b/examples/http_api/src/todos/routes/mod.rs @@ -0,0 +1,3 @@ +mod todos; + +pub use todos::basic_routes; diff --git a/examples/http_api/src/todos/routes/todos.rs b/examples/http_api/src/todos/routes/todos.rs new file mode 100644 index 0000000..e5518a1 --- /dev/null +++ b/examples/http_api/src/todos/routes/todos.rs @@ -0,0 +1,18 @@ +use crate::todos::controllers; +use actix_web::web::{self, ServiceConfig}; +use http_components::CustomServiceConfigure; + +pub fn basic_routes() -> CustomServiceConfigure { + CustomServiceConfigure::new(|cfg: &mut ServiceConfig| { + cfg.service( + web::scope("/v1/todos") + // If you would like to add authentication middleware for all this routes bellow, just use the middleware as follow: + // + // .wrap(http_components::middlewares::authentication::AuthenticationMiddleware) + .service(controllers::post) + .service(controllers::list) + .service(controllers::get) + .service(controllers::delete), + ); + }) +} diff --git a/examples/http_api/src/todos/viewmodels/mod.rs b/examples/http_api/src/todos/viewmodels/mod.rs new file mode 100644 index 0000000..1d133ef --- /dev/null +++ b/examples/http_api/src/todos/viewmodels/mod.rs @@ -0,0 +1,3 @@ +mod todos; + +pub use todos::{ToDoRequest, ToDoResponse}; diff --git a/examples/http_api/src/todos/viewmodels/todos.rs b/examples/http_api/src/todos/viewmodels/todos.rs new file mode 100644 index 0000000..cacc83c --- /dev/null +++ b/examples/http_api/src/todos/viewmodels/todos.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct ToDoRequest {} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct ToDoResponse {}