diff --git a/Cargo.toml b/Cargo.toml index faf60ba13b..2e7b0e4b3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -171,6 +171,7 @@ tokio-tungstenite = "0.20.1" tracing = "0.1.37" tungstenite = { version = "0.20.1", default-features = false } ts-rs = "7.0.0" +utoipa = "3.5.0" url = "2.4" zeroize = "1.6.0" diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index 5e817b6afd..34cd570a60 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -29,6 +29,7 @@ opentelemetry-jaeger = { version = "0.18.0", optional = true, features = [ "isahc_collector_client", ] } tracing-opentelemetry = { version = "0.19.0", optional = true } +utoipa = { workspace = true, optional = true } opentelemetry = { version = "0.19.0", optional = true, features = ["rt-tokio"] } @@ -42,6 +43,7 @@ vergen = { version = "=7.4.3", default-features = false, features = [ [features] default = [] +openapi = ["utoipa"] output_format = ["serde_json"] tracing = [ "tracing-subscriber", diff --git a/common/bin-common/src/build_information/mod.rs b/common/bin-common/src/build_information/mod.rs index 064f8a60fa..34d0aaf456 100644 --- a/common/bin-common/src/build_information/mod.rs +++ b/common/bin-common/src/build_information/mod.rs @@ -81,6 +81,7 @@ impl BinaryBuildInformation { } #[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct BinaryBuildInformationOwned { /// Provides the name of the binary, i.e. the content of `CARGO_PKG_NAME` environmental variable. pub binary_name: String, diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index e8eec0332c..90ce2f6704 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -15,16 +15,20 @@ anyhow = { workspace = true } bytes = "1.5.0" serde = { workspace = true, features = ["derive"] } serde_yaml = "0.9.25" +serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } # HTTP API: axum = { workspace = true } + +# useful for `#[axum_macros::debug_handler]` +#axum-macros = "0.3.8" mime = "0.3.17" hyper = { workspace = true } tower = { version = "0.4.13" } tower-http = { version = "0.4.4", features = ["fs"] } -utoipa = { version = "3.5.0", features = ["axum_extras"] } +utoipa = { workspace = true, features = ["axum_extras"] } utoipa-swagger-ui = { version = "3.1.5", features = ["axum"] } # if we ever wanted redoc/rapidoc bridges: @@ -32,4 +36,4 @@ utoipa-swagger-ui = { version = "3.1.5", features = ["axum"] } #utoipa-rapidoc = { version = "0.1.0", features = ["axum"] } -nym-bin-common = { path = "../common/bin-common" } +nym-bin-common = { path = "../common/bin-common", features = ["openapi"] } diff --git a/nym-node/src/http/router/api/mod.rs b/nym-node/src/http/router/api/mod.rs index 8e06bd3e57..4ead438c35 100644 --- a/nym-node/src/http/router/api/mod.rs +++ b/nym-node/src/http/router/api/mod.rs @@ -1,11 +1,13 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::http::state::AppState; use axum::http::{header, HeaderValue, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::{Json, Router}; use bytes::{BufMut, BytesMut}; use serde::{Deserialize, Serialize}; +use utoipa::{IntoParams, ToSchema}; pub mod v1; @@ -19,13 +21,31 @@ pub struct Config { pub v1_config: v1::Config, } -pub(super) fn routes(config: Config) -> Router { - Router::new().nest_service(routes::V1, v1::routes(config.v1_config)) +pub(super) fn routes(config: Config) -> Router { + Router::new().nest(routes::V1, v1::routes(config.v1_config)) // .nest(routes::SWAGGER, openapi::route()) // .nest(routes::SWAGGER, openapi::route()) } -#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] +#[derive(Debug, Clone, ToSchema)] +pub enum FormattedResponse { + Json(Json), + Yaml(Yaml), +} + +impl IntoResponse for FormattedResponse +where + T: Serialize, +{ + fn into_response(self) -> Response { + match self { + FormattedResponse::Json(json_response) => json_response.into_response(), + FormattedResponse::Yaml(yaml_response) => yaml_response.into_response(), + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone, ToSchema)] #[serde(rename_all = "lowercase")] pub enum Output { #[default] @@ -33,11 +53,17 @@ pub enum Output { Yaml, } +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)] +#[serde(default)] +pub struct OutputParams { + pub output: Option, +} + impl Output { - pub fn to_response(self, data: T) -> Box { + pub fn to_response(self, data: T) -> FormattedResponse { match self { - Output::Json => Box::new(Json(data)), - Output::Yaml => Box::new(Yaml(data)), + Output::Json => FormattedResponse::Json(Json(data)), + Output::Yaml => FormattedResponse::Yaml(Yaml(data)), } } } diff --git a/nym-node/src/http/router/api/v1/build_info.rs b/nym-node/src/http/router/api/v1/build_info.rs index cd64992588..531beb77eb 100644 --- a/nym-node/src/http/router/api/v1/build_info.rs +++ b/nym-node/src/http/router/api/v1/build_info.rs @@ -1,22 +1,33 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::http::router::api::Output; -use axum::extract::Query; +use crate::http::router::api::{FormattedResponse, OutputParams}; +use crate::http::state::AppState; +use axum::extract::{Query, State}; use axum::response::IntoResponse; +use nym_bin_common::build_information::BinaryBuildInformationOwned; +/// Description of the path #[utoipa::path( get, - path = "/", + path = "/build-info", + context_path = "/api/v1", responses( - (status=200, description ="", body = ()) + (status=200, content( + ("application/json" = BinaryBuildInformationOwned), + ("application/yaml" = BinaryBuildInformationOwned) + )) ), - params( - ("output" = Option, Query, description = "") - ) + params(OutputParams) )] -pub async fn build_info(Query(output): Query>) -> impl IntoResponse { - let output = output.unwrap_or_default(); - - todo!() +pub async fn build_info( + State(state): State, + Query(output): Query, +) -> FormattedResponse { + let output = output.output.unwrap_or_default(); + // todo!() + // TODO: get rid of the clone since it's getting serialized anyway + output.to_response(state.build_information.clone()) } + +pub struct BuildInfoResponse(BinaryBuildInformationOwned); diff --git a/nym-node/src/http/router/api/v1/gateway/mod.rs b/nym-node/src/http/router/api/v1/gateway/mod.rs index 3c58ea081b..e8619ab838 100644 --- a/nym-node/src/http/router/api/v1/gateway/mod.rs +++ b/nym-node/src/http/router/api/v1/gateway/mod.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::http::state::AppState; use axum::http::StatusCode; use axum::routing::get; use axum::Router; @@ -8,6 +9,6 @@ use axum::Router; #[derive(Debug, Clone, Default)] pub struct Config {} -pub(crate) fn routes(_config: Config) -> Router { +pub(crate) fn routes(_config: Config) -> Router { Router::new().route("/", get(|| async { StatusCode::NOT_IMPLEMENTED })) } diff --git a/nym-node/src/http/router/api/v1/mixnode/mod.rs b/nym-node/src/http/router/api/v1/mixnode/mod.rs index 3c58ea081b..e8619ab838 100644 --- a/nym-node/src/http/router/api/v1/mixnode/mod.rs +++ b/nym-node/src/http/router/api/v1/mixnode/mod.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::http::state::AppState; use axum::http::StatusCode; use axum::routing::get; use axum::Router; @@ -8,6 +9,6 @@ use axum::Router; #[derive(Debug, Clone, Default)] pub struct Config {} -pub(crate) fn routes(_config: Config) -> Router { +pub(crate) fn routes(_config: Config) -> Router { Router::new().route("/", get(|| async { StatusCode::NOT_IMPLEMENTED })) } diff --git a/nym-node/src/http/router/api/v1/mod.rs b/nym-node/src/http/router/api/v1/mod.rs index 07b8a1cfdc..0d52e4bc22 100644 --- a/nym-node/src/http/router/api/v1/mod.rs +++ b/nym-node/src/http/router/api/v1/mod.rs @@ -3,6 +3,7 @@ use crate::http::router::api::v1::build_info::build_info; use crate::http::router::api::v1::roles::roles; +use crate::http::state::AppState; use axum::routing::get; use axum::Router; use nym_bin_common::build_information::BinaryBuildInformationOwned; @@ -30,7 +31,7 @@ pub struct Config { pub network_requester: network_requester::Config, } -pub(super) fn routes(config: Config) -> Router { +pub(super) fn routes(config: Config) -> Router { Router::new() .nest(routes::GATEWAY, gateway::routes(config.gateway)) .nest(routes::MIXNODE, mixnode::routes(config.mixnide)) diff --git a/nym-node/src/http/router/api/v1/network_requester/mod.rs b/nym-node/src/http/router/api/v1/network_requester/mod.rs index 3c58ea081b..e8619ab838 100644 --- a/nym-node/src/http/router/api/v1/network_requester/mod.rs +++ b/nym-node/src/http/router/api/v1/network_requester/mod.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::http::state::AppState; use axum::http::StatusCode; use axum::routing::get; use axum::Router; @@ -8,6 +9,6 @@ use axum::Router; #[derive(Debug, Clone, Default)] pub struct Config {} -pub(crate) fn routes(_config: Config) -> Router { +pub(crate) fn routes(_config: Config) -> Router { Router::new().route("/", get(|| async { StatusCode::NOT_IMPLEMENTED })) } diff --git a/nym-node/src/http/router/api/v1/openapi.rs b/nym-node/src/http/router/api/v1/openapi.rs index cd1fb53891..7b84b60c1b 100644 --- a/nym-node/src/http/router/api/v1/openapi.rs +++ b/nym-node/src/http/router/api/v1/openapi.rs @@ -1,15 +1,22 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::http::router::api; +use crate::http::state::AppState; use axum::Router; +use nym_bin_common::build_information::BinaryBuildInformationOwned; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; #[derive(OpenApi)] -#[openapi(info(title = "NymNode API"))] +#[openapi( + info(title = "NymNode API"), + paths(api::v1::build_info::build_info,), + components(schemas(api::Output, api::OutputParams, BinaryBuildInformationOwned)) +)] pub(crate) struct ApiDoc; -pub(crate) fn route() -> Router { +pub(crate) fn route() -> Router { // provide absolute path to the openapi.json let config = utoipa_swagger_ui::Config::from("/api/v1/api-docs/openapi.json"); SwaggerUi::new("/swagger") diff --git a/nym-node/src/http/router/landing_page.rs b/nym-node/src/http/router/landing_page.rs index 340df1126d..75f5f7f6af 100644 --- a/nym-node/src/http/router/landing_page.rs +++ b/nym-node/src/http/router/landing_page.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::http::state::AppState; use axum::routing::get; use axum::Router; use std::path::PathBuf; @@ -11,7 +12,7 @@ pub struct Config { pub assets_path: Option, } -pub(super) fn routes(config: Config) -> Router { +pub(super) fn routes(config: Config) -> Router { if let Some(assets) = config.assets_path { Router::new().nest_service("/", ServeDir::new(assets)) } else { diff --git a/nym-node/src/http/router/mod.rs b/nym-node/src/http/router/mod.rs index 81b212e4e7..8277de8f46 100644 --- a/nym-node/src/http/router/mod.rs +++ b/nym-node/src/http/router/mod.rs @@ -3,6 +3,7 @@ use crate::error::NymNodeError; use crate::http::middleware::logging; +use crate::http::state::AppState; use axum::routing::IntoMakeService; use axum::Router; use hyper::server::conn::AddrIncoming; @@ -47,7 +48,8 @@ impl NymNodeRouter { .nest(routes::LANDING_PAGE, landing_page::routes(config.landing)) .nest(routes::POLICY, policy::routes(config.policy)) .nest(routes::API, api::routes(config.api)) - .layer(axum::middleware::from_fn(logging::logger)), + .layer(axum::middleware::from_fn(logging::logger)) + .with_state(AppState::new_dummy()), } } diff --git a/nym-node/src/http/router/policy.rs b/nym-node/src/http/router/policy.rs index 672bddb78c..3d89cd0c21 100644 --- a/nym-node/src/http/router/policy.rs +++ b/nym-node/src/http/router/policy.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::http::state::AppState; use axum::routing::get; use axum::Router; use std::path::PathBuf; @@ -11,7 +12,7 @@ pub struct Config { pub assets_path: Option, } -pub(super) fn routes(config: Config) -> Router { +pub(super) fn routes(config: Config) -> Router { if let Some(assets) = config.assets_path { Router::new().nest_service("/", ServeDir::new(assets)) } else { diff --git a/nym-node/src/http/state.rs b/nym-node/src/http/state.rs index 26a1fb3dcb..d3ac6ce479 100644 --- a/nym-node/src/http/state.rs +++ b/nym-node/src/http/state.rs @@ -1,16 +1,36 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_bin_common::bin_info_owned; use nym_bin_common::build_information::BinaryBuildInformationOwned; +use std::ops::Deref; use std::sync::Arc; #[derive(Debug, Clone)] -pub(crate) struct AppState { +pub struct AppState { inner: Arc, } +impl AppState { + pub fn new_dummy() -> Self { + AppState { + inner: Arc::new(AppStateInner { + build_information: bin_info_owned!(), + }), + } + } +} + +impl Deref for AppState { + type Target = AppStateInner; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + #[derive(Debug)] -struct AppStateInner { +pub struct AppStateInner { // TODO: split it based on routes? pub(crate) build_information: BinaryBuildInformationOwned, }