Skip to content

Commit

Permalink
Improve api (#45)
Browse files Browse the repository at this point in the history
* Improve utoipa-swagger-ui remove unnecessary clone
* Improve utoipa-gen -> implement Response and Header ToTokens traits
* Add support for multiple content types for response body
* Add external_docs for OpenApi
  • Loading branch information
juhaku authored Mar 17, 2022
1 parent c3f7626 commit fd2d7c8
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 124 deletions.
6 changes: 6 additions & 0 deletions src/openapi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ impl OpenApi {
self
}

pub fn with_external_docs(mut self, external_docs: ExternalDocs) -> Self {
self.external_docs = Some(external_docs);

self
}

#[cfg(feature = "serde_json")]
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
Expand Down
31 changes: 31 additions & 0 deletions tests/openapi_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,34 @@ fn derive_openapi_tags() {
"tags.[1].externalDocs.description" = r###""Find more about pets""###, "Tags pets_api external docs description"
}
}

#[test]
fn derive_openapi_with_external_docs() {
#[derive(OpenApi)]
#[openapi(external_docs(
url = "http://localhost.more.about.api",
description = "Find out more"
))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();

assert_value! {doc=>
"externalDocs.url" = r###""http://localhost.more.about.api""###, "External docs url"
"externalDocs.description" = r###""Find out more""###, "External docs description"
}
}

#[test]
fn derive_openapi_with_external_docs_only_url() {
#[derive(OpenApi)]
#[openapi(external_docs(url = "http://localhost.more.about.api"))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();

assert_value! {doc=>
"externalDocs.url" = r###""http://localhost.more.about.api""###, "External docs url"
"externalDocs.description" = r###"null"###, "External docs description"
}
}
21 changes: 21 additions & 0 deletions tests/path_response_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,24 @@ fn derive_response_with_json_example_success() {
"responses.200.headers" = r#"null"#, "Response headers"
}
}

#[test]
fn derive_reponse_multiple_content_types() {
test_fn! {
module: response_multiple_content_types,
responses: (
(status = 200, description = "success", body = Foo, content_type = ["text/xml", "application/json"])
)
}

let doc = api_doc!(module: response_multiple_content_types);

assert_value! {doc=>
"responses.200.description" = r#""success""#, "Response description"
"responses.200.content.application/json.schema.$ref" = r###""#/components/schemas/Foo""###, "Response content ref"
"responses.200.content.text/xml.schema.$ref" = r###""#/components/schemas/Foo""###, "Response content ref"
"responses.200.content.application/json.example" = r###"null"###, "Response content example"
"responses.200.content.text/xml.example" = r###"null"###, "Response content example"
"responses.200.headers" = r#"null"#, "Response headers"
}
}
14 changes: 13 additions & 1 deletion utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
/// from the `body` attribute. If defined the value should be valid content type such as
/// _`application/json`_. By default the content type is _`text/plain`_ for
/// [primitive Rust types][primitive] and _`application/json`_ for struct and complex enum types.
/// Content type can also be slice of **content_type** values if the endpoint support returning multiple
/// response content types. E.g _`["application/json", "text/xml"]`_ would indicate that endpoint can return both
/// _`json`_ and _`xml`_ formats.
/// * **headers** Slice of response headers that are returned back to a caller.
/// * **example** Can be either `json!(...)` or literal str that can be parsed to json. `json!`
/// should be something that `serde_json::json!` can parse as a `serde_json::Value`. [^json]
Expand All @@ -282,6 +285,11 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
/// )
/// ```
///
/// **Response with multiple response content types:**
/// ```text
/// (status = 200, description = "Success response", body = Pet, content_type = ["application/json", "text/xml"])
/// ```
///
/// # Response Header Attributes
///
/// * **name** Name of the header. E.g. _`x-csrf-token`_
Expand Down Expand Up @@ -504,6 +512,8 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
/// the tag is derived from path given to **handlers** list or if undefined then `crate` is used by default.
/// Alternatively the tag name can be given to path operation via [`#[utoipa::path(...)]`][path] macro.
/// Tag can be used to define extra information for the api to produce richer documentation.
/// * **external_docs** Can be used to reference external resource to the OpenAPI doc for extended documentation.
/// External docs can be in [`OpenApi`][openapi_struct] or in [`Tag`][tags] level.
///
/// OpenApi derive macro will also derive [`Info`][info] for OpenApi specification using Cargo
/// environment variables.
Expand Down Expand Up @@ -556,12 +566,14 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
/// tags(
/// (name = "pets::api", description = "All about pets",
/// external_docs(url = "http://more.about.pets.api", description = "Find out more"))
/// )
/// ),
/// external_docs(url = "http://more.about.our.apis", description = "More about our APIs")
/// )]
/// struct ApiDoc;
/// ```
///
/// [openapi]: trait.OpenApi.html
/// [openapi_struct]: openapi/struct.OpenApi.html
/// [component]: derive.Component.html
/// [path]: attr.path.html
/// [modify]: trait.Modify.html
Expand Down
16 changes: 14 additions & 2 deletions utoipa-gen/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub struct OpenApiAttr {
modifiers: Punctuated<Modifier, Comma>,
security: Option<Array<SecurityRequirementAttr>>,
tags: Option<Array<Tag>>,
external_docs: Option<ExternalDocs>,
}

pub fn parse_openapi_attrs(attrs: &[Attribute]) -> Option<OpenApiAttr> {
Expand All @@ -38,7 +39,7 @@ pub fn parse_openapi_attrs(attrs: &[Attribute]) -> Option<OpenApiAttr> {
impl Parse for OpenApiAttr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
const EXPECTED_ATTRIBUTE: &str =
"unexpected attribute, expected any of: handlers, components, modifiers, security, tags";
"unexpected attribute, expected any of: handlers, components, modifiers, security, tags, external_docs";
let mut openapi = OpenApiAttr::default();

while !input.is_empty() {
Expand Down Expand Up @@ -67,6 +68,11 @@ impl Parse for OpenApiAttr {
parenthesized!(tags in input);
openapi.tags = Some(parse_utils::parse_groups(&tags)?);
}
"external_docs" => {
let external_docs;
parenthesized!(external_docs in input);
openapi.external_docs = Some(external_docs.parse()?);
}
_ => {
return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE));
}
Expand Down Expand Up @@ -256,6 +262,11 @@ impl ToTokens for OpenApi {
.with_tags(#tags)
}
});
let external_docs = attributes.external_docs.as_ref().map(|external_docs| {
quote! {
.with_external_docs(#external_docs)
}
});

tokens.extend(quote! {
impl utoipa::OpenApi for #ident {
Expand All @@ -264,7 +275,8 @@ impl ToTokens for OpenApi {
let mut openapi = utoipa::openapi::OpenApi::new(#info, #path_items)
.with_components(#components)
#securities
#tags;
#tags
#external_docs;

let _mods: [&dyn utoipa::Modify; #modifiers_len] = [#modifiers];
_mods.iter().for_each(|modifier| modifier.modify(&mut openapi));
Expand Down
Loading

0 comments on commit fd2d7c8

Please sign in to comment.