Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nest OpenApi support #930

Merged
merged 1 commit into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[workspace.package]
rust-version = "1.75"

[workspace]
resolver = "2"
members = [
Expand Down
1 change: 1 addition & 0 deletions utoipa-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ readme = "README.md"
keywords = ["openapi", "codegen", "proc-macro", "documentation", "compile-time"]
repository = "https://github.com/juhaku/utoipa"
authors = ["Juha Kukkonen <[email protected]>"]
rust-version.workspace = true

[lib]
proc-macro = true
Expand Down
9 changes: 2 additions & 7 deletions utoipa-gen/src/component/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ impl SerdeValue {

/// The [Serde Enum representation](https://serde.rs/enum-representations.html) being used
/// The default case (when no serde attributes are present) is `ExternallyTagged`.
#[derive(Clone)]
#[derive(Clone, Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum SerdeEnumRepr {
#[default]
ExternallyTagged,
InternallyTagged {
tag: String,
Expand All @@ -108,12 +109,6 @@ pub enum SerdeEnumRepr {
},
}

impl Default for SerdeEnumRepr {
fn default() -> SerdeEnumRepr {
SerdeEnumRepr::ExternallyTagged
}
}

/// Attributes defined within a `#[serde(...)]` container attribute.
#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
Expand Down
42 changes: 40 additions & 2 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ use self::{
/// the OpenAPI. E.g _`as = path::to::Pet`_. This would make the schema appear in the generated
/// OpenAPI spec as _`path.to.Pet`_.
/// * `default` Can be used to populate default values on all fields using the struct's
/// [`Default`](std::default::Default) implementation.
/// [`Default`] implementation.
/// * `deprecated` Can be used to mark all fields as deprecated in the generated OpenAPI spec but
/// not in the code. If you'd like to mark the fields as deprecated in the code as well use
/// Rust's own `#[deprecated]` attribute instead.
Expand Down Expand Up @@ -131,7 +131,7 @@ use self::{
/// * `example = ...` Can be method reference or _`json!(...)`_.
/// * `default = ...` Can be method reference or _`json!(...)`_. If no value is specified, and the struct has
/// only one field, the field's default value in the schema will be set from the struct's
/// [`Default`](std::default::Default) implementation.
/// [`Default`] implementation.
/// * `format = ...` May either be variant of the [`KnownFormat`][known_format] enum, or otherwise
/// an open value as a string. By default the format is derived from the type of the property
/// according OpenApi spec.
Expand Down Expand Up @@ -1456,6 +1456,18 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
/// whole attribute from generated values of Cargo environment variables. E.g. defining
/// `contact(name = ...)` will ultimately override whole contact of info and not just partially
/// the name.
/// * `nest(...)` Allows nesting [`OpenApi`][openapi_struct]s to this _`OpenApi`_ instance. Nest
/// takes comma separated list of tuples that define comma separated key values of nest path and
/// _`OpenApi`_ instance to nest. _`OpenApi`_ instance must implement [`OpenApi`][openapi] trait.
///
/// _**Nest syntax example.**_
///
/// ```text
/// nest(
/// ("/path/to/nest", ApiToNest),
/// ("/another", AnotherApi)
/// )
/// ```
///
/// OpenApi derive macro will also derive [`Info`][info] for OpenApi specification using Cargo
/// environment variables.
Expand Down Expand Up @@ -1610,6 +1622,32 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
/// struct ApiDoc;
/// ```
///
/// _**Nest _`UserApi`_ to the current api doc instance.**_
/// ```rust
/// # use utoipa::OpenApi;
/// #
/// #[utoipa::path(get, path = "/api/v1/status")]
/// fn test_path_status() {}
///
/// #[utoipa::path(get, path = "/test")]
/// fn user_test_path() {}
///
/// #[derive(OpenApi)]
/// #[openapi(paths(user_test_path))]
/// struct UserApi;
///
/// #[derive(OpenApi)]
/// #[openapi(
/// paths(
/// test_path_status
/// ),
/// nest(
/// ("/api/v1/user", UserApi),
/// )
/// )]
/// struct ApiDoc;
/// ```
///
/// [openapi]: trait.OpenApi.html
/// [openapi_struct]: openapi/struct.OpenApi.html
/// [to_schema]: derive.ToSchema.html
Expand Down
55 changes: 54 additions & 1 deletion utoipa-gen/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct OpenApiAttr<'o> {
tags: Option<Array<'static, Tag>>,
external_docs: Option<ExternalDocs>,
servers: Punctuated<Server, Comma>,
nested: Vec<NestOpenApi>,
}

impl<'o> OpenApiAttr<'o> {
Expand Down Expand Up @@ -77,7 +78,7 @@ pub fn parse_openapi_attrs(attrs: &[Attribute]) -> Result<Option<OpenApiAttr>, E
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, external_docs, servers";
"unexpected attribute, expected any of: handlers, components, modifiers, security, tags, external_docs, servers, nest";
let mut openapi = OpenApiAttr::default();

while !input.is_empty() {
Expand Down Expand Up @@ -119,6 +120,11 @@ impl Parse for OpenApiAttr<'_> {
"servers" => {
openapi.servers = parse_utils::parse_punctuated_within_parenthesis(input)?;
}
"nest" => {
let nest;
parenthesized!(nest in input);
openapi.nested = parse_utils::parse_groups(&nest)?;
}
_ => {
return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE));
}
Expand Down Expand Up @@ -387,6 +393,31 @@ impl Parse for ServerVariable {

pub(crate) struct OpenApi<'o>(pub OpenApiAttr<'o>, pub Ident);

impl OpenApi<'_> {
fn nested_tokens(&self) -> Option<TokenStream> {
if self.0.nested.is_empty() {
None
} else {
let nest_tokens = self
.0
.nested
.iter()
.map(|item| {
let path = &item.path;
let nest_api = &item.open_api;

let span = nest_api.span();
quote_spanned! {span=>
.nest(#path, <#nest_api as utoipa::OpenApi>::openapi())
}
})
.collect::<TokenStream>();

Some(nest_tokens)
}
}
}

impl ToTokens for OpenApi<'_> {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let OpenApi(attributes, ident) = self;
Expand Down Expand Up @@ -428,6 +459,9 @@ impl ToTokens for OpenApi<'_> {
None
};

let nested_tokens = self
.nested_tokens()
.map(|tokens| quote! {openapi = openapi #tokens;});
tokens.extend(quote! {
impl utoipa::OpenApi for #ident {
fn openapi() -> utoipa::openapi::OpenApi {
Expand All @@ -441,6 +475,7 @@ impl ToTokens for OpenApi<'_> {
#servers
#external_docs
.build();
#nested_tokens

let _mods: [&dyn utoipa::Modify; #modifiers_len] = [#modifiers];
_mods.iter().for_each(|modifier| modifier.modify(&mut openapi));
Expand Down Expand Up @@ -570,3 +605,21 @@ fn impl_paths(handler_paths: &Punctuated<ExprPath, Comma>) -> TokenStream {
},
)
}

#[cfg_attr(feature = "debug", derive(Debug))]
struct NestOpenApi {
path: String,
open_api: TypePath,
}

impl Parse for NestOpenApi {
fn parse(input: ParseStream) -> syn::Result<Self> {
let path = input.parse::<LitStr>()?;
input.parse::<Comma>()?;
let api = input.parse::<TypePath>()?;
Ok(Self {
path: path.value(),
open_api: api,
})
}
}
75 changes: 74 additions & 1 deletion utoipa-gen/tests/openapi_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use serde::Serialize;
use serde_json::{json, Value};
use utoipa::{
openapi::{RefOr, Response, ResponseBuilder},
OpenApi, ToResponse,
Modify, OpenApi, ToResponse,
};
use utoipa_gen::ToSchema;

Expand Down Expand Up @@ -378,3 +378,76 @@ fn derive_openapi_with_generic_schema_with_as() {
})
)
}

#[test]
fn derive_nest_openapi_with_modifier() {
struct MyModifier;
impl Modify for MyModifier {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
openapi.info.description = Some("this is description".to_string())
}
}

#[utoipa::path(get, path = "/api/v1/status")]
#[allow(dead_code)]
fn test_path_status() {}

#[utoipa::path(get, path = "/test")]
#[allow(dead_code)]
fn user_test_path() {}

#[derive(OpenApi)]
#[openapi(paths(user_test_path))]
struct UserApi;

#[utoipa::path(get, path = "/")]
#[allow(dead_code)]
fn foobar() {}

#[derive(OpenApi)]
#[openapi(paths(foobar))]
struct FooBarApi;

#[derive(OpenApi)]
#[openapi(
paths(
test_path_status
),
modifiers(&MyModifier),
nest(
("/api/v1/user", UserApi),
("/api/v1/foobar", FooBarApi)
)
)]
struct ApiDoc;

let api = serde_json::to_value(&ApiDoc::openapi()).expect("should serialize to value");
let paths = api.pointer("/paths");

assert_json_eq!(
paths,
json!({
"/api/v1/foobar/": {
"get": {
"operationId": "foobar",
"responses": {},
"tags": [ "crate" ]
}
},
"/api/v1/status": {
"get": {
"operationId": "test_path_status",
"responses": {},
"tags": [ "crate" ]
}
},
"/api/v1/user/test": {
"get": {
"operationId": "user_test_path",
"responses": {},
"tags": [ "crate" ]
}
}
})
)
}
1 change: 1 addition & 0 deletions utoipa-rapidoc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ keywords = ["rapidoc", "openapi", "documentation"]
repository = "https://github.com/juhaku/utoipa"
categories = ["web-programming"]
authors = ["Juha Kukkonen <[email protected]>"]
rust-version.workspace = true

[package.metadata.docs.rs]
features = ["actix-web", "axum", "rocket"]
Expand Down
1 change: 1 addition & 0 deletions utoipa-redoc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ keywords = ["redoc", "openapi", "documentation"]
repository = "https://github.com/juhaku/utoipa"
categories = ["web-programming"]
authors = ["Juha Kukkonen <[email protected]>"]
rust-version.workspace = true

[package.metadata.docs.rs]
features = ["actix-web", "axum", "rocket"]
Expand Down
1 change: 1 addition & 0 deletions utoipa-scalar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ keywords = ["scalar", "openapi", "documentation"]
repository = "https://github.com/juhaku/utoipa"
categories = ["web-programming"]
authors = ["Juha Kukkonen <[email protected]>"]
rust-version.workspace = true

[package.metadata.docs.rs]
features = ["actix-web", "axum", "rocket"]
Expand Down
1 change: 1 addition & 0 deletions utoipa-swagger-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ keywords = ["swagger-ui", "openapi", "documentation"]
repository = "https://github.com/juhaku/utoipa"
categories = ["web-programming"]
authors = ["Juha Kukkonen <[email protected]>"]
rust-version.workspace = true

[features]
debug = []
Expand Down
1 change: 1 addition & 0 deletions utoipa/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ keywords = [
repository = "https://github.com/juhaku/utoipa"
categories = ["web-programming"]
authors = ["Juha Kukkonen <[email protected]>"]
rust-version.workspace = true

[features]
# See README.md for list and explanations of features
Expand Down
Loading
Loading