Skip to content

Commit

Permalink
Add support for property_names for object
Browse files Browse the repository at this point in the history
Add support for defining `property_names` of object such as a map. This
commit allows users to define completely typed maps where value is
defined via `additional_properties` and key is defined via
`property_names`.
```rust
 #[derive(ToSchema)]
 enum Names {
     Foo,
     Bar,
 }

 struct Mapped(std::collections::BTreeMap<Names, String>);

 // will render to following json
 json!({
     "propertyNames": {
         "type": "string",
         "enum": ["Foo", "Bar"]
     },
     "additionalProperties": {
         "type": "string"
     },
     "type": "object"
 })
```

Closes #755
  • Loading branch information
juhaku committed Oct 3, 2024
1 parent d5e722a commit ba68744
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 19 deletions.
1 change: 1 addition & 0 deletions utoipa-gen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

* Add support for `property_names` for object (https://github.com/juhaku/utoipa/pull/1084)
* Add `bound` attribute for customizing generic impl bounds. (https://github.com/juhaku/utoipa/pull/1079)
* Add auto collect schemas for utoipa-axum (https://github.com/juhaku/utoipa/pull/1072)
* Add global config for `utiopa` (https://github.com/juhaku/utoipa/pull/1048)
Expand Down
35 changes: 28 additions & 7 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,14 +821,34 @@ impl ComponentSchema {
.as_ref()
.map_try(|feature| Ok(as_tokens_or_diagnostics!(feature)))?
.or_else_try(|| {
let children = type_tree
.children
.as_ref()
.expect("ComponentSchema Map type should have chidren");
// Get propertyNames
let property_name = children
.first()
.expect("ComponentSchema Map type shouldu have 2 child, getting first");
let property_name_alias = property_name.get_alias_type()?;
let property_name_alias =
property_name_alias.as_ref().map_try(TypeTree::from_type)?;
let property_name_child = property_name_alias.as_ref().unwrap_or(property_name);

let mut property_name_features = features.clone();
property_name_features.push(Feature::Inline(true.into()));
let property_name_schema = ComponentSchema::new(ComponentSchemaProps {
container,
type_tree: property_name_child,
features: property_name_features,
description: None,
})?;
let property_name_tokens = property_name_schema.to_token_stream();

// Maps are treated as generic objects with no named properties and
// additionalProperties denoting the type
// maps have 2 child schemas and we are interested the second one of them
// which is used to determine the additional properties
let child = type_tree
.children
.as_ref()
.expect("ComponentSchema Map type should have children")
let child = children
.get(1)
.expect("ComponentSchema Map type should have 2 child");
let alias = child.get_alias_type()?;
Expand All @@ -845,9 +865,10 @@ impl ComponentSchema {

schema_references.extend(schema_property.schema_references);

Result::<Option<TokenStream>, Diagnostics>::Ok(Some(
quote! { .additional_properties(Some(#schema_tokens)) },
))
Result::<Option<TokenStream>, Diagnostics>::Ok(Some(quote! {
.property_names(Some(#property_name_tokens))
.additional_properties(Some(#schema_tokens))
}))
})?;

let schema_type =
Expand Down
33 changes: 21 additions & 12 deletions utoipa-gen/tests/path_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,21 +622,27 @@ fn derive_path_params_map() {
"name": "with_ref",
"required": true,
"schema": {
"additionalProperties": {
"$ref": "#/components/schemas/Foo"
},
"type": "object"
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"$ref": "#/components/schemas/Foo"
},
"type": "object"
}
},
{
"in": "path",
"name": "with_type",
"required": true,
"schema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"type": "object"
}
}
]}
Expand Down Expand Up @@ -666,10 +672,13 @@ fn derive_path_params_with_examples() {
"key": "value"
},
"schema": {
"additionalProperties": {
"type": "string"
},
"type": "object"
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"type": "object"
}
},
{
Expand Down
62 changes: 62 additions & 0 deletions utoipa-gen/tests/schema_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,18 @@ fn derive_map_ref() {
json!({
"properties": {
"map": {
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"$ref": "#/components/schemas/Foo"
},
"type": "object",
},
"map2": {
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string",
"enum": ["Variant"]
Expand Down Expand Up @@ -328,6 +334,9 @@ fn derive_struct_with_default_attr_field() {
]
},
"leases": {
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"$ref": "#/components/schemas/Book",
},
Expand Down Expand Up @@ -430,6 +439,9 @@ fn derive_struct_with_optional_properties() {
},
"metadata": {
"type": ["object", "null"],
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
Expand Down Expand Up @@ -508,6 +520,9 @@ fn derive_struct_with_comments() {
"map": {
"description": "Map description",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
Expand Down Expand Up @@ -4180,6 +4195,9 @@ fn derive_struct_field_with_example() {
},
"field3": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string",
},
Expand All @@ -4189,6 +4207,9 @@ fn derive_struct_field_with_example() {
},
"field4": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"$ref": "#/components/schemas/MyStruct",
},
Expand Down Expand Up @@ -4738,6 +4759,9 @@ fn derive_struct_with_deprecated_fields() {
}
},
"map": {
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
Expand Down Expand Up @@ -4799,6 +4823,9 @@ fn derive_struct_with_schema_deprecated_fields() {
}
},
"map": {
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
Expand Down Expand Up @@ -5003,7 +5030,13 @@ fn derive_schema_with_unit_hashmap() {
json!({
"properties": {
"volumes": {
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"propertyNames": {
"default": null,
},
"additionalProperties": {
"default": null,
},
Expand Down Expand Up @@ -5619,3 +5652,32 @@ fn derive_negative_numbers() {
})
}
}

#[test]
fn derive_map_with_property_names() {
#![allow(unused)]

#[derive(ToSchema)]
enum Names {
Foo,
Bar,
}

let value = api_doc! {
struct Mapped(std::collections::BTreeMap<Names, String>);
};

assert_json_eq!(
value,
json!({
"propertyNames": {
"type": "string",
"enum": ["Foo", "Bar"]
},
"additionalProperties": {
"type": "string"
},
"type": "object"
})
)
}
1 change: 1 addition & 0 deletions utoipa/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

* Add support for `property_names` for object (https://github.com/juhaku/utoipa/pull/1084)
* Add auto collect schemas for utoipa-axum (https://github.com/juhaku/utoipa/pull/1072)
* Add global config for `utiopa` (https://github.com/juhaku/utoipa/pull/1048)
* Add support for `links` in `#[utoipa::path]` (https://github.com/juhaku/utoipa/pull/1047)
Expand Down
22 changes: 22 additions & 0 deletions utoipa/src/openapi/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,11 @@ builder! {
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<Box<AdditionalProperties<Schema>>>,

/// Additional [`Schema`] to describe property names of an object such as a map. See more
/// details <https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#name-propertynames>
#[serde(skip_serializing_if = "Option::is_none")]
pub property_names: Option<Box<Schema>>,

/// Changes the [`Object`] deprecated status.
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<Deprecated>,
Expand Down Expand Up @@ -1102,6 +1107,12 @@ impl ObjectBuilder {
set_value!(self additional_properties additional_properties.map(|additional_properties| Box::new(additional_properties.into())))
}

/// Add additional [`Schema`] to describe property names of an object such as a map. See more
/// details <https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#name-propertynames>
pub fn property_names<S: Into<Schema>>(mut self, property_name: Option<S>) -> Self {
set_value!(self property_names property_name.map(|property_name| Box::new(property_name.into())))
}

/// Add field to the required fields of [`Object`].
pub fn required<I: Into<String>>(mut self, required_field: I) -> Self {
self.required.push(required_field.into());
Expand Down Expand Up @@ -1250,6 +1261,17 @@ impl From<ObjectBuilder> for RefOr<Schema> {
}
}

impl From<RefOr<Schema>> for Schema {
fn from(value: RefOr<Schema>) -> Self {
match value {
RefOr::Ref(_) => {
panic!("Invalid type `RefOr::Ref` provided, cannot convert to RefOr::T<Schema>")
}
RefOr::T(value) => value,
}
}
}

/// AdditionalProperties is used to define values of map fields of the [`Schema`].
///
/// The value can either be [`RefOr`] or _`bool`_.
Expand Down

0 comments on commit ba68744

Please sign in to comment.