Skip to content

Commit

Permalink
Add OpenAPI Serve object types
Browse files Browse the repository at this point in the history
* Add openapi Server object types -> Add builders, docs, tests.
  • Loading branch information
juhaku committed Apr 1, 2022
1 parent 4363b88 commit c995c46
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 18 deletions.
21 changes: 20 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@
//!
//! [security]: openapi/security/index.html
/// Rust implementation of Openapi Spec V3
pub mod openapi;

pub use utoipa_gen::*;
Expand Down Expand Up @@ -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);
}
4 changes: 3 additions & 1 deletion src/openapi.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Rust implementation of Openapi Spec V3
use serde::{de::Visitor, Deserialize, Serialize, Serializer};

pub use self::{
Expand All @@ -13,7 +15,7 @@ pub use self::{
Ref, ToArray,
},
security::SecurityRequirement,
server::Server,
server::{Server, ServerBuilder, ServerVariable, ServerVariableBuilder},
tag::Tag,
};

Expand Down
242 changes: 227 additions & 15 deletions src/openapi/server.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
builder! {
ServerBuilder;

#[serde(skip_serializing_if = "Option::is_none")]
pub variables: Option<HashMap<String, String>>, // 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<String>,

/// Optional map of variable name and its substitution value used in [`Server::url`].
#[serde(skip_serializing_if = "Option::is_none")]
pub variables: Option<HashMap<String, ServerVariable>>,
}
}

impl Server {
pub fn new<S: AsRef<str>>(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<S: Into<String>>(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<U: Into<String>>(mut self, url: U) -> Self {
set_value!(self url url.into())
}

pub fn with_description<S: AsRef<str>>(mut self, description: S) -> Self {
self.description = Some(description.as_ref().to_string());
/// Add or change description of the [`Server`].
pub fn description<S: Into<String>>(mut self, description: Option<S>) -> 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<N: Into<String>, V: Into<ServerVariable>>(
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<String>,

/// 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<Vec<String>>,
}
}

impl ServerVariableBuilder {
/// Add default value for substitution.
pub fn default_value<S: Into<String>>(mut self, default_value: S) -> Self {
set_value!(self default_value default_value.into())
}

/// Add or change description of substituted parameter.
pub fn description<S: Into<String>>(mut self, description: Option<S>) -> Self {
set_value!(self description description.map(|description| description.into()))
}

/// Add or change possible values used to substitute parameter.
pub fn enum_values<I: IntoIterator<Item = V>, V: Into<String>>(
mut self,
enum_values: Option<I>,
) -> 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"
}
}
}"###
}
}
11 changes: 10 additions & 1 deletion tests/utoipa_gen_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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()
]);
}
}

Expand Down

0 comments on commit c995c46

Please sign in to comment.