Skip to content

Commit

Permalink
Feature enhanced generics via aliases (#144)
Browse files Browse the repository at this point in the history
* Add type aliases for `Component`s to support generic components. 
* Add tests and update docs
  • Loading branch information
juhaku authored May 24, 2022
1 parent e2b7dc0 commit dc6bf6b
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 23 deletions.
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ pub trait OpenApi {
/// ```
pub trait Component {
fn component() -> openapi::schema::Component;

fn aliases() -> Vec<(&'static str, openapi::schema::Component)> {
Vec::new()
}
}

/// Trait for implementing OpenAPI PathItem object with path.
Expand Down
33 changes: 33 additions & 0 deletions src/openapi/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,39 @@ impl ComponentsBuilder {
self
}

/// Add [`Component`]s from iterator.
///
/// # Examples
/// ```rust
/// # use utoipa::openapi::schema::{ComponentsBuilder, ObjectBuilder,
/// # PropertyBuilder, ComponentType};
/// ComponentsBuilder::new().components_from_iter([(
/// "Pet",
/// ObjectBuilder::new()
/// .property(
/// "name",
/// PropertyBuilder::new().component_type(ComponentType::String),
/// )
/// .required("name"),
/// )]);
/// ```
pub fn components_from_iter<
I: IntoIterator<Item = (S, C)>,
C: Into<Component>,
S: Into<String>,
>(
mut self,
components: I,
) -> Self {
self.schemas.extend(
components
.into_iter()
.map(|(name, component)| (name.into(), component.into())),
);

self
}

/// Add [`SecurityScheme`] to [`Components`].
///
/// Accepts two arguments where first is the name of the [`SecurityScheme`]. This is later when
Expand Down
24 changes: 24 additions & 0 deletions tests/component_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -886,3 +886,27 @@ fn derive_component_with_generic_types_having_path_expression() {
"properties.args.type" = r#""array""#, "Args type"
}
}

#[test]
fn derive_component_with_aliases() {
struct A;

#[derive(Debug, OpenApi)]
#[openapi(components(MyAlias))]
struct ApiDoc;

#[derive(Component)]
#[aliases(MyAlias = Bar<A>)]
struct Bar<R> {
#[allow(dead_code)]
bar: R,
}

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

let value = common::get_json_path(doc_value, "components.schemas");
assert_value! {value=>
"MyAlias.properties.bar.$ref" = r###""#/components/schemas/A""###, "MyAlias aliased property"
}
}
19 changes: 18 additions & 1 deletion tests/utoipa_gen_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ mod pet_api {
#[derive(Default, OpenApi)]
#[openapi(
handlers(pet_api::get_pet_by_id),
components(Pet),
components(Pet, GenericC, GenericD),
modifiers(&Foo),
security(
(),
Expand All @@ -99,6 +99,23 @@ macro_rules! build_foo {
};
}

#[derive(Deserialize, Serialize, Component)]
struct A {
a: String,
}

#[derive(Deserialize, Serialize, Component)]
struct B {
b: i64,
}

#[derive(Deserialize, Serialize, Component)]
#[aliases(GenericC = C<A, B>, GenericD = C<B, A>)]
struct C<T, R> {
field_1: R,
field_2: T,
}

#[test]
#[ignore = "this is just a test bed to run macros"]
fn derive_openapi() {
Expand Down
43 changes: 36 additions & 7 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use crate::path::{Path, PathAttr};
use ext::ArgumentResolver;

#[proc_macro_error]
#[proc_macro_derive(Component, attributes(component))]
#[proc_macro_derive(Component, attributes(component, aliases))]
/// Component derive macro
///
/// This is `#[derive]` implementation for [`Component`][c] trait. The macro accepts one `component`
Expand All @@ -58,18 +58,18 @@ use ext::ArgumentResolver;
/// OpenAPI. OpenAPI has only a boolean flag to determine deprecation. While it is totally okay to declare deprecated with reason
/// `#[deprecated = "There is better way to do this"]` the reason would not render in OpenAPI spec.
///
/// # Struct Optional Configuration Options
/// # Struct Optional Configuration Options for `#[component(...)]`
/// * `example = ...` Can be either _`json!(...)`_ or literal string that can be parsed to json. _`json!`_
/// should be something that _`serde_json::json!`_ can parse as a _`serde_json::Value`_. [^json]
/// * `xml(...)` Can be used to define [`Xml`][xml] object properties applicable to Structs.
///
/// [^json]: **json** feature need to be enabled for _`json!(...)`_ type to work.
///
/// # Enum Optional Configuration Options
/// # Enum Optional Configuration Options for `#[component(...)]`
/// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
/// * `default = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
///
/// # Unnamed Field Struct Optional Configuration Options
/// # Unnamed Field Struct Optional Configuration Options for `#[component(...)]`
/// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
/// * `default = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
/// * `format = ...` [`ComponentFormat`][format] to use for the property. By default the format is derived from
Expand All @@ -79,7 +79,7 @@ use ext::ArgumentResolver;
/// any third-party types are used which are not components nor primitive types. With **value_type** we can enforce
/// type used to certain type. Value type may only be [`primitive`][primitive] type or [`String`]. Generic types are not allowed.
///
/// # Named Fields Optional Configuration Options
/// # Named Fields Optional Configuration Options for `#[component(...)]`
/// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
/// * `default = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
/// * `format = ...` [`ComponentFormat`][format] to use for the property. By default the format is derived from
Expand Down Expand Up @@ -139,6 +139,35 @@ use ext::ArgumentResolver;
/// }
/// ```
///
/// # Generic components with aliases
///
/// Components can also be generic which allows reusing types. This enables certain behaviour patters
/// where super type delcares common code for type aliases.
///
/// In this example we have common `Status` type which accepts one generic type. It is then defined
/// with `#[aliases(...)]` that it is going to be used with [`std::string::String`] and [`i32`] values.
/// The generic argument could also be another [`Component`][c] as well.
/// ```rust
/// # use utoipa::{Component, OpenApi};
/// #[derive(Component)]
/// #[aliases(StatusMessage = Status<String>, StatusNumber = Status<i32>)]
/// struct Status<T> {
/// value: T
/// }
///
/// #[derive(OpenApi)]
/// #[openapi(
/// components(StatusMessage, StatusNumber)
/// )]
/// struct ApiDoc;
/// ```
///
/// The `#[aliases(...)]` is just syntatic sugar and will create Rust [type aliases](https://doc.rust-lang.org/reference/items/type-aliases.html)
/// behind the scenes which then can be later referenced anywhere in code.
///
/// **Note!** You should never register generic type itself in `components(...)` so according above example `Status<...>` should not be registered
/// because it will not render the type correctly and will cause an error in generated OpenAPI spec.
///
/// # Examples
///
/// Example struct with struct level example.
Expand Down Expand Up @@ -269,10 +298,10 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
ident,
data,
generics,
..
vis,
} = syn::parse_macro_input!(input);

let component = Component::new(&data, &attrs, &ident, &generics);
let component = Component::new(&data, &attrs, &ident, &generics, &vis);

component.to_token_stream().into()
}
Expand Down
1 change: 1 addition & 0 deletions utoipa-gen/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ fn impl_components(
};
schema.extend(quote! {
.component(#component_name, <#ident #ty_generics>::component())
.components_from_iter(<#ident #ty_generics>::aliases())
});

schema
Expand Down
36 changes: 32 additions & 4 deletions utoipa-gen/src/schema.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::rc::Rc;

use proc_macro2::Ident;
use proc_macro_error::{abort, abort_call_site};
use syn::{
Expand Down Expand Up @@ -33,7 +31,7 @@ struct ComponentPart<'a> {
pub ident: &'a Ident,
pub value_type: ValueType,
pub generic_type: Option<GenericType>,
pub child: Option<Rc<ComponentPart<'a>>>,
pub child: Option<Box<ComponentPart<'a>>>,
}

impl<'a> ComponentPart<'a> {
Expand Down Expand Up @@ -105,7 +103,7 @@ impl<'a> ComponentPart<'a> {

let mut generic_component_type = ComponentPart::convert(&segment.ident, segment);

generic_component_type.child = Some(Rc::new(ComponentPart::from_type(
generic_component_type.child = Some(Box::new(ComponentPart::from_type(
match &segment.arguments {
PathArguments::AngleBracketed(angle_bracketed_args) => {
ComponentPart::get_generic_arg_type(0, angle_bracketed_args)
Expand Down Expand Up @@ -161,6 +159,36 @@ impl<'a> ComponentPart<'a> {
_ => None,
}
}

fn find_mut_by_ident(&mut self, ident: &'a Ident) -> Option<&mut Self> {
match self.generic_type {
Some(GenericType::Map) => None,
Some(GenericType::Vec)
| Some(GenericType::Option)
| Some(GenericType::Cow)
| Some(GenericType::Box)
| Some(GenericType::RefCell) => {
Self::find_mut_by_ident(self.child.as_mut().unwrap().as_mut(), ident)
}
None => {
if ident == self.ident {
Some(self)
} else {
None
}
}
}
}

fn update_ident(&mut self, ident: &'a Ident) {
self.ident = ident
}
}

impl<'a> AsMut<ComponentPart<'a>> for ComponentPart<'a> {
fn as_mut(&mut self) -> &mut ComponentPart<'a> {
self
}
}

#[cfg_attr(feature = "debug", derive(Debug))]
Expand Down
Loading

0 comments on commit dc6bf6b

Please sign in to comment.