diff --git a/src/lib.rs b/src/lib.rs index 9766c88f..b60b4d5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,7 +164,6 @@ //! //! [security]: openapi/security/index.html -/// Rust implementation of Openapi Spec V3 pub mod openapi; pub use utoipa_gen::*; @@ -409,6 +408,26 @@ pub trait Path { /// } /// } /// ``` +/// +/// Add [OpenAPI Server Object][server] to alter the target server url. This can be used to give context +/// path for api operations. +/// ```rust +/// # use utoipa::{OpenApi, Modify}; +/// # use utoipa::openapi::Server; +/// #[derive(OpenApi)] +/// #[openapi(modifiers(&ServerAddon))] +/// struct ApiDoc; +/// +/// struct ServerAddon; +/// +/// impl Modify for ServerAddon { +/// fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { +/// openapi.servers = Some(vec![Server::new("/api")]) +/// } +/// } +/// ``` +/// +/// [server]: https://spec.openapis.org/oas/latest.html#server-object pub trait Modify { fn modify(&self, openapi: &mut openapi::OpenApi); } diff --git a/src/openapi.rs b/src/openapi.rs index fdf3f08f..c70a0a5c 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -1,3 +1,5 @@ +//! Rust implementation of Openapi Spec V3 + use serde::{de::Visitor, Deserialize, Serialize, Serializer}; pub use self::{ @@ -13,7 +15,7 @@ pub use self::{ Ref, ToArray, }, security::SecurityRequirement, - server::Server, + server::{Server, ServerBuilder, ServerVariable, ServerVariableBuilder}, tag::Tag, }; diff --git a/src/openapi/server.rs b/src/openapi/server.rs index 1728d6a9..999e01e5 100644 --- a/src/openapi/server.rs +++ b/src/openapi/server.rs @@ -1,32 +1,244 @@ -use std::collections::HashMap; +//! Implements [OpenAPI Server Object][server] types to configure target servers. +//! +//! OpenAPI will implicitly add [`Server`] with `url = "/"` to [`OpenApi`][openapi] when no servers +//! are defined. +//! +//! [`Server`] can be used to alter connection url for _**path operations**_. It can be a +//! relative path e.g `/api/v1` or valid http url e.g. `http://alternative.api.com/api/v1`. +//! +//! Relative path will append to the **sever address** so the connection url for _**path operations**_ +//! will become `server address + relative path`. +//! +//! Optionally it also supports parmater substitution with `{variable}` syntax. +//! +//! See [`Modify`][modify] trait for details how add servers to [`OpenApi`][openapi]. +//! +//! # Examples +//! +//! Create new server with relative path. +//! ```rust +//! # use utoipa::openapi::server::Server; +//! Server::new("/api/v1"); +//! ``` +//! +//! Create server with custom url using a builder. +//! ```rust +//! # use utoipa::openapi::server::ServerBuilder; +//! ServerBuilder::new().url("https://alternative.api.url.test/api").build(); +//! ``` +//! +//! Create server with builder and variable substitution. +//! ```rust +//! # use utoipa::openapi::server::{ServerBuilder, ServerVariableBuilder}; +//! ServerBuilder::new().url("/api/{version}/{username}") +//! .parameter("version", ServerVariableBuilder::new() +//! .enum_values(Some(["v1", "v2"])) +//! .default_value("v1")) +//! .parameter("username", ServerVariableBuilder::new() +//! .default_value("the_user")).build(); +//! ``` +//! +//! [server]: https://spec.openapis.org/oas/latest.html#server-object +//! [openapi]: ../struct.OpenApi.html +//! [modify]: ../../trait.Modify.html +use std::{collections::HashMap, iter}; use serde::{Deserialize, Serialize}; -#[non_exhaustive] -#[derive(Serialize, Deserialize, Default, Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] -#[serde(rename_all = "camelCase")] -pub struct Server { - pub url: String, +use super::{build_fn, builder, from, new, set_value}; - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, +builder! { + ServerBuilder; - #[serde(skip_serializing_if = "Option::is_none")] - pub variables: Option>, // TODO check the correct type here + /// Represents target server object. It can be used to alter server connection for + /// _**path operations**_. + /// + /// By default OpenAPI will implicitly implement [`Server`] with `url = "/"` if no servers is provided to + /// the [`OpenApi`][openapi]. + /// + /// [openapi]: ../struct.OpenApi.html + #[non_exhaustive] + #[derive(Serialize, Deserialize, Default, Clone)] + #[cfg_attr(feature = "debug", derive(Debug))] + #[serde(rename_all = "camelCase")] + pub struct Server { + /// Target url of the [`Server`]. It can be valid http url or relative path. + /// + /// Url also supports variable substitution with `{variable}` syntax. The substitutions + /// then can be configured with [`Server::variables`] map. + pub url: String, + + /// Optional description describing the target server url. Description supports markdown syntax. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// Optional map of variable name and its substitution value used in [`Server::url`]. + #[serde(skip_serializing_if = "Option::is_none")] + pub variables: Option>, + } } impl Server { - pub fn new>(url: S) -> Self { + /// Construct a new [`Server`] with given url. Url can be valid http url or context path of the url. + /// + /// If url is valid http url then all path operation request's will be forwarded to the selected [`Server`]. + /// + /// If url is path of url e.g. `/api/v1` then the url will be appended to the servers address and the + /// operations will be forwarded to location `server address + url`. + /// + /// + /// # Examples + /// + /// Create new server with url path. + /// ```rust + /// # use utoipa::openapi::server::Server; + /// Server::new("/api/v1"); + /// ``` + /// + /// Create new server with alternative server. + /// ```rust + /// # use utoipa::openapi::server::Server; + /// Server::new("https://alternative.pet-api.test/api/v1"); + /// ``` + pub fn new>(url: S) -> Self { Self { - url: url.as_ref().to_string(), + url: url.into(), ..Default::default() } } +} + +impl ServerBuilder { + /// Add url to the target [`Server`]. + pub fn url>(mut self, url: U) -> Self { + set_value!(self url url.into()) + } - pub fn with_description>(mut self, description: S) -> Self { - self.description = Some(description.as_ref().to_string()); + /// Add or change description of the [`Server`]. + pub fn description>(mut self, description: Option) -> Self { + set_value!(self description description.map(|description| description.into())) + } + + /// Add parameter to [`Server`] which is used to substitute values in [`Server::url`]. + /// + /// * `name` Defines name of the parmaeter which is being substituted within the url. If url has + /// `{username}` substitution then the name should be `username`. + /// * `parameter` Use [`ServerVariableBuilder`] to define how the parameter is being substituted + /// within the url. + pub fn parameter, V: Into>( + mut self, + name: N, + variable: V, + ) -> Self { + match self.variables { + Some(ref mut variables) => { + variables.insert(name.into(), variable.into()); + } + None => { + self.variables = Some(HashMap::from_iter(iter::once(( + name.into(), + variable.into(), + )))) + } + } self } } + +builder! { + ServerVariableBuilder; + + /// Implements [OpenAPI Server Variable][server_variable] used to substitute varibles in [`Server::url`]. + /// + /// [server_variable]: https://spec.openapis.org/oas/latest.html#server-variable-object + #[non_exhaustive] + #[derive(Serialize, Deserialize, Default, Clone)] + #[cfg_attr(feature = "debug", derive(Debug))] + pub struct ServerVariable { + /// Default value used to substitute parameter if no other value is being provided. + #[serde(rename = "default")] + default_value: String, + + /// Optional description descringing the variable of substitution. Markdown syntax is supported. + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + + /// Enum values can be used to limit possible options for substitution. If enum values is used + /// the [`ServerVariable::default_value`] must contain one of the enum values. + #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] + enum_values: Option>, + } +} + +impl ServerVariableBuilder { + /// Add default value for substitution. + pub fn default_value>(mut self, default_value: S) -> Self { + set_value!(self default_value default_value.into()) + } + + /// Add or change description of substituted parameter. + pub fn description>(mut self, description: Option) -> Self { + set_value!(self description description.map(|description| description.into())) + } + + /// Add or change possible values used to substitute parameter. + pub fn enum_values, V: Into>( + mut self, + enum_values: Option, + ) -> Self { + set_value!(self enum_values enum_values + .map(|enum_values| enum_values.into_iter().map(|value| value.into()).collect())) + } +} + +#[cfg(feature = "json")] +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! test_fn { + ($name:ident: $schema:expr; $expected:literal) => { + #[test] + fn $name() { + let value = serde_json::to_value($schema).unwrap(); + let expected_value: serde_json::Value = serde_json::from_str($expected).unwrap(); + + assert_eq!( + value, + expected_value, + "testing serializing \"{}\": \nactual:\n{}\nexpected:\n{}", + stringify!($name), + value, + expected_value + ); + + println!("{}", &serde_json::to_string_pretty(&$schema).unwrap()); + } + }; + } + + test_fn! { + create_server_with_builder_and_variable_substitution: + ServerBuilder::new().url("/api/{version}/{username}") + .parameter("version", ServerVariableBuilder::new() + .enum_values(Some(["v1", "v2"])) + .description(Some("api version")) + .default_value("v1")) + .parameter("username", ServerVariableBuilder::new() + .default_value("the_user")).build(); + r###"{ + "url": "/api/{version}/{username}", + "variables": { + "version": { + "enum": ["v1", "v2"], + "default": "v1", + "description": "api version" + }, + "username": { + "default": "the_user" + } + } +}"### + } +} diff --git a/tests/utoipa_gen_test.rs b/tests/utoipa_gen_test.rs index 8d35cd0c..7b80e210 100644 --- a/tests/utoipa_gen_test.rs +++ b/tests/utoipa_gen_test.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use utoipa::{ openapi::{ self, - security::{HttpAuthScheme, SecuritySchema, HttpBuilder}, + security::{HttpAuthScheme, SecuritySchema, HttpBuilder}, server::{ServerBuilder, ServerVariableBuilder}, }, Component, Modify, OpenApi, }; @@ -108,6 +108,15 @@ impl Modify for Foo { ), ) } + + + openapi.servers = Some(vec![ + ServerBuilder::new() + .url("/api/bar/{username}") + .description(Some("this is description of the server")) + .parameter("username", + ServerVariableBuilder::new().default_value("the_user").description(Some("this is user"))).build() + ]); } }