Skip to content

Commit

Permalink
Add support for title (#224)
Browse files Browse the repository at this point in the history
Add support for `title` property ([OpenAPI Schema title](https://swagger.io/specification/#schema-object)) for struct and enum `Component` properties.

The title is useful for enums because currently the generator for rust 
clients created a struct for each variant and name them like:

    my_enum_one_of_1
    my_enum_one_of_2
    etc

So with the title you can at least control the name of each variant structure. 
There is an open issue to actually produce an enum but it's not been worked 
on for a while OpenAPITools/openapi-generator#9497.
  • Loading branch information
Sytten authored Jul 24, 2022
1 parent ad2b076 commit eeb0dd4
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 16 deletions.
42 changes: 38 additions & 4 deletions src/openapi/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ pub struct Property {
#[serde(rename = "type")]
pub component_type: ComponentType,

/// Changes the [`Property`] title.
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,

/// Additional format for detailing the component type.
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<ComponentFormat>,
Expand Down Expand Up @@ -358,6 +362,8 @@ impl ToArray for Property {}
pub struct PropertyBuilder {
component_type: ComponentType,

title: Option<String>,

format: Option<ComponentFormat>,

description: Option<String>,
Expand Down Expand Up @@ -386,7 +392,7 @@ pub struct PropertyBuilder {
}

from!(Property PropertyBuilder
component_type, format, description, default, enum_values, example, deprecated, write_only, read_only, xml);
component_type, title, format, description, default, enum_values, example, deprecated, write_only, read_only, xml);

impl PropertyBuilder {
new!(pub PropertyBuilder);
Expand All @@ -396,6 +402,11 @@ impl PropertyBuilder {
set_value!(self component_type component_type)
}

/// Add or change the title of the [`Property`].
pub fn title<I: Into<String>>(mut self, title: Option<I>) -> Self {
set_value!(self title title.map(|title| title.into()))
}

/// Add or change additional format for detailing the component type.
pub fn format(mut self, format: Option<ComponentFormat>) -> Self {
set_value!(self format format)
Expand Down Expand Up @@ -462,7 +473,7 @@ impl PropertyBuilder {
to_array_builder!();

build_fn!(pub Property
component_type, format, description, default, enum_values, example, deprecated, write_only, read_only, xml);
component_type, title, format, description, default, enum_values, example, deprecated, write_only, read_only, xml);
}

component_from_builder!(PropertyBuilder);
Expand All @@ -480,6 +491,10 @@ pub struct Object {
#[serde(rename = "type")]
component_type: ComponentType,

/// Changes the [`Object`] title.
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,

/// Vector of required field names.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub required: Vec<String>,
Expand Down Expand Up @@ -539,6 +554,8 @@ impl ToArray for Object {}
pub struct ObjectBuilder {
component_type: ComponentType,

title: Option<String>,

required: Vec<String>,

properties: BTreeMap<String, Component>,
Expand Down Expand Up @@ -589,6 +606,11 @@ impl ObjectBuilder {
self
}

/// Add or change the title of the [`Object`].
pub fn title<I: Into<String>>(mut self, title: Option<I>) -> Self {
set_value!(self title title.map(|title| title.into()))
}

/// Add or change description of the property. Markdown syntax is supported.
pub fn description<I: Into<String>>(mut self, description: Option<I>) -> Self {
set_value!(self description description.map(|description| description.into()))
Expand Down Expand Up @@ -618,10 +640,10 @@ impl ObjectBuilder {

to_array_builder!();

build_fn!(pub Object component_type, required, properties, description, deprecated, example, xml, additional_properties);
build_fn!(pub Object component_type, title, required, properties, description, deprecated, example, xml, additional_properties);
}

from!(Object ObjectBuilder component_type, required, properties, description, deprecated, example, xml, additional_properties);
from!(Object ObjectBuilder component_type, title, required, properties, description, deprecated, example, xml, additional_properties);
component_from_builder!(ObjectBuilder);

/// Implements [OpenAPI Reference Object][reference] that can be used to reference
Expand Down Expand Up @@ -986,6 +1008,18 @@ mod tests {
)
}

#[test]
fn test_object_with_title() {
let json_value = ObjectBuilder::new().title(Some("SomeName")).build();
assert_json_eq!(
json_value,
json!({
"type": "object",
"title": "SomeName"
})
);
}

#[test]
fn derive_object_with_example() {
let expected = r#"{"type":"object","example":{"age":20,"name":"bob the cat"}}"#;
Expand Down
122 changes: 122 additions & 0 deletions tests/component_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,67 @@ fn derive_complex_enum() {
);
}

#[test]
fn derive_complex_enum_title() {
#[derive(Serialize)]
struct Foo(String);

let value: Value = api_doc! {
#[derive(Serialize)]
enum Bar {
#[component(title = "Unit")]
UnitValue,
#[component(title = "Named")]
NamedFields {
id: &'static str,
},
#[component(title = "Unnamed")]
UnnamedFields(Foo),
}
};

assert_json_eq!(
value,
json!({
"oneOf": [
{
"type": "string",
"title": "Unit",
"enum": [
"UnitValue",
],
},
{
"type": "object",
"title": "Named",
"properties": {
"NamedFields": {
"type": "object",
"properties": {
"id": {
"type": "string",
},
},
"required": [
"id",
],
},
},
},
{
"type": "object",
"title": "Unnamed",
"properties": {
"UnnamedFields": {
"$ref": "#/components/schemas/Foo",
},
},
},
],
})
);
}

#[test]
fn derive_complex_enum_serde_rename_all() {
#[derive(Serialize)]
Expand Down Expand Up @@ -995,6 +1056,67 @@ fn derive_complex_enum_serde_tag() {
);
}

#[test]
fn derive_complex_enum_serde_tag_title() {
#[derive(Serialize)]
struct Foo(String);

let value: Value = api_doc! {
#[derive(Serialize)]
#[serde(tag = "tag")]
enum Bar {
#[component(title = "Unit")]
UnitValue,
#[component(title = "Named")]
NamedFields {
id: &'static str,
},
}
};

assert_json_eq!(
value,
json!({
"oneOf": [
{
"type": "object",
"title": "Unit",
"properties": {
"tag": {
"type": "string",
"enum": [
"UnitValue",
],
},
},
"required": [
"tag",
],
},
{
"type": "object",
"title": "Named",
"properties": {
"id": {
"type": "string",
},
"tag": {
"type": "string",
"enum": [
"NamedFields",
],
},
},
"required": [
"id",
"tag",
],
},
],
})
);
}

#[test]
fn derive_struct_with_read_only_and_write_only() {
let user = api_doc! {
Expand Down
29 changes: 28 additions & 1 deletion utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,18 @@ use ext::ArgumentResolver;
/// }
/// ```
///
/// It is possible to specify the title of each variant to help generators create named structures.
/// ```rust
/// # use utoipa::Component;
/// #[derive(Component)]
/// enum ErrorResponse {
/// #[component(title = "InvalidCredentials")]
/// InvalidCredentials,
/// #[component(title = "NotFound")]
/// NotFound(String),
/// }
/// ```
///
/// Use `xml` attribute to manipulate xml output.
/// ```rust
/// # use utoipa::Component;
Expand Down Expand Up @@ -1663,7 +1675,7 @@ impl ToTokens for AnyValue {

/// Parsing utils
mod parse_utils {
use proc_macro2::{Group, Ident, TokenStream};
use proc_macro2::{Group, Ident, TokenStream, TokenTree};
use proc_macro_error::ResultExt;
use syn::{
parenthesized,
Expand All @@ -1673,6 +1685,21 @@ mod parse_utils {
Error, LitBool, LitStr, Token,
};

pub fn skip_past_next_comma(input: ParseStream) -> syn::Result<()> {
input.step(|cursor| {
let mut rest = *cursor;
while let Some((tt, next)) = rest.token_tree() {
match &tt {
TokenTree::Punct(punct) if punct.as_char() == ',' => {
return Ok(((), next));
}
_ => rest = next,
}
}
Ok(((), rest))
})
}

pub fn parse_next<T: Sized>(input: ParseStream, next: impl FnOnce() -> T) -> T {
input
.parse::<Token![=]>()
Expand Down
Loading

0 comments on commit eeb0dd4

Please sign in to comment.