mirror of
https://github.com/poem-web/poem.git
synced 2026-01-25 04:18:25 +00:00
Add support to externally tagged unions (#1043)
* chore: add track_caller to test asserts * support externally-tagged unions * new line * add derive error when discriminator_name and externally tagger are both specified * refactor: bind pattern variable
This commit is contained in:
@@ -40,6 +40,8 @@ struct UnionArgs {
|
||||
#[darling(default)]
|
||||
discriminator_name: Option<String>,
|
||||
#[darling(default)]
|
||||
externally_tagged: bool,
|
||||
#[darling(default)]
|
||||
external_docs: Option<ExternalDocument>,
|
||||
#[darling(default)]
|
||||
rename_all: Option<RenameRule>,
|
||||
@@ -55,11 +57,18 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
let description = optional_literal(&description);
|
||||
let discriminator_name = &args.discriminator_name;
|
||||
|
||||
let e = match &args.data {
|
||||
Data::Enum(e) => e,
|
||||
_ => return Err(Error::new_spanned(ident, "AnyOf can only be applied to an enum.").into()),
|
||||
let Data::Enum(e) = &args.data else {
|
||||
return Err(Error::new_spanned(ident, "AnyOf can only be applied to an enum.").into());
|
||||
};
|
||||
|
||||
if discriminator_name.is_some() && args.externally_tagged {
|
||||
return Err(Error::new_spanned(
|
||||
ident,
|
||||
"Discriminator name cannot be used with externally tagged unions.",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let mut types = Vec::new();
|
||||
let mut from_json = Vec::new();
|
||||
let mut to_json = Vec::new();
|
||||
@@ -76,122 +85,157 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
for variant in e {
|
||||
let item_ident = &variant.ident;
|
||||
|
||||
match variant.fields.len() {
|
||||
1 => {
|
||||
let object_ty = &variant.fields.fields[0];
|
||||
let schema_name = quote! {
|
||||
::std::format!("{}_{}", <Self as #crate_name::types::Type>::name(), <#object_ty as #crate_name::types::Type>::name())
|
||||
};
|
||||
let mapping_name = match &variant.mapping {
|
||||
Some(mapping) => quote!(::std::string::ToString::to_string(#mapping)),
|
||||
None => {
|
||||
let name = apply_rename_rule_variant(
|
||||
args.rename_all,
|
||||
item_ident.unraw().to_string(),
|
||||
);
|
||||
quote!(::std::string::ToString::to_string(#name))
|
||||
if variant.fields.len() != 1 {
|
||||
return Err(Error::new_spanned(&variant.ident, "Incorrect oneof definition.").into());
|
||||
}
|
||||
|
||||
let object_ty = &variant.fields.fields[0];
|
||||
let schema_name = quote! {
|
||||
::std::format!("{}_{}", <Self as #crate_name::types::Type>::name(), <#object_ty as #crate_name::types::Type>::name())
|
||||
};
|
||||
let mapping_name = match &variant.mapping {
|
||||
Some(mapping) => mapping.clone(),
|
||||
None => apply_rename_rule_variant(args.rename_all, item_ident.unraw().to_string()),
|
||||
};
|
||||
types.push(object_ty);
|
||||
|
||||
if args.externally_tagged {
|
||||
from_json.push(quote! {
|
||||
if let value @ ::std::option::Option::Some(_) = value.as_object().and_then(|obj| obj.get(#mapping_name)).cloned() {
|
||||
return <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(value)
|
||||
.map(Self::#item_ident)
|
||||
.map_err(#crate_name::types::ParseError::propagate);
|
||||
}
|
||||
});
|
||||
} else if discriminator_name.is_some() {
|
||||
from_json.push(quote! {
|
||||
if ::std::matches!(discriminator_name, ::std::option::Option::Some(discriminator_name) if discriminator_name == #mapping_name) {
|
||||
return <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(::std::option::Option::Some(value))
|
||||
.map(Self::#item_ident)
|
||||
.map_err(#crate_name::types::ParseError::propagate);
|
||||
}
|
||||
});
|
||||
} else if !args.one_of {
|
||||
// any of
|
||||
from_json.push(quote! {
|
||||
if let ::std::result::Result::Ok(obj) = <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(::std::option::Option::Some(::std::clone::Clone::clone(&value)))
|
||||
.map(Self::#item_ident) {
|
||||
return ::std::result::Result::Ok(obj);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// one of
|
||||
from_json.push(quote! {
|
||||
if let ::std::result::Result::Ok(obj) = <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(::std::option::Option::Some(::std::clone::Clone::clone(&value)))
|
||||
.map(Self::#item_ident) {
|
||||
if res_obj.is_some() {
|
||||
return ::std::result::Result::Err(#crate_name::types::ParseError::expected_type(value));
|
||||
}
|
||||
};
|
||||
types.push(object_ty);
|
||||
res_obj = Some(obj);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if discriminator_name.is_some() {
|
||||
from_json.push(quote! {
|
||||
if ::std::matches!(discriminator_name, ::std::option::Option::Some(discriminator_name) if discriminator_name == &#mapping_name) {
|
||||
return <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(::std::option::Option::Some(value))
|
||||
.map(Self::#item_ident)
|
||||
.map_err(#crate_name::types::ParseError::propagate);
|
||||
}
|
||||
});
|
||||
} else if !args.one_of {
|
||||
// any of
|
||||
from_json.push(quote! {
|
||||
if let ::std::result::Result::Ok(obj) = <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(::std::option::Option::Some(::std::clone::Clone::clone(&value)))
|
||||
.map(Self::#item_ident) {
|
||||
return ::std::result::Result::Ok(obj);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// one of
|
||||
from_json.push(quote! {
|
||||
if let ::std::result::Result::Ok(obj) = <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(::std::option::Option::Some(::std::clone::Clone::clone(&value)))
|
||||
.map(Self::#item_ident) {
|
||||
if res_obj.is_some() {
|
||||
return ::std::result::Result::Err(#crate_name::types::ParseError::expected_type(value));
|
||||
}
|
||||
res_obj = Some(obj);
|
||||
}
|
||||
});
|
||||
if args.externally_tagged {
|
||||
to_json.push(quote! {
|
||||
Self::#item_ident(obj) => {
|
||||
let value = <#object_ty as #crate_name::types::ToJSON>::to_json(obj);
|
||||
let mut wrapped = #crate_name::__private::serde_json::Map::new();
|
||||
wrapped.insert(::std::convert::Into::into(#mapping_name), ::std::option::Option::unwrap_or_default(value));
|
||||
::std::option::Option::Some(#crate_name::__private::serde_json::Value::Object(wrapped))
|
||||
}
|
||||
});
|
||||
} else if let Some(discriminator_name) = &discriminator_name {
|
||||
to_json.push(quote! {
|
||||
Self::#item_ident(obj) => {
|
||||
let mut value = <#object_ty as #crate_name::types::ToJSON>::to_json(obj);
|
||||
if let ::std::option::Option::Some(obj) = value.as_mut().and_then(|value| value.as_object_mut()) {
|
||||
obj.insert(::std::convert::Into::into(#discriminator_name), ::std::convert::Into::into(#mapping_name));
|
||||
}
|
||||
value
|
||||
}
|
||||
});
|
||||
} else {
|
||||
to_json.push(quote! {
|
||||
Self::#item_ident(obj) => <#object_ty as #crate_name::types::ToJSON>::to_json(obj)
|
||||
});
|
||||
}
|
||||
|
||||
mapping.push(quote! {
|
||||
(::std::string::ToString::to_string(#mapping_name), ::std::format!("#/components/schemas/{}", #schema_name))
|
||||
});
|
||||
|
||||
if args.externally_tagged {
|
||||
create_schemas.push(quote! {
|
||||
{
|
||||
fn __check_is_object_type<T: #crate_name::types::IsObjectType>() {}
|
||||
__check_is_object_type::<#object_ty>();
|
||||
}
|
||||
|
||||
if let Some(discriminator_name) = &discriminator_name {
|
||||
to_json.push(quote! {
|
||||
Self::#item_ident(obj) => {
|
||||
let mut value = <#object_ty as #crate_name::types::ToJSON>::to_json(obj);
|
||||
if let ::std::option::Option::Some(obj) = value.as_mut().and_then(|value| value.as_object_mut()) {
|
||||
obj.insert(::std::convert::Into::into(#discriminator_name), ::std::convert::Into::into(#mapping_name));
|
||||
}
|
||||
value
|
||||
}
|
||||
});
|
||||
} else {
|
||||
to_json.push(quote! {
|
||||
Self::#item_ident(obj) => <#object_ty as #crate_name::types::ToJSON>::to_json(obj)
|
||||
});
|
||||
}
|
||||
|
||||
mapping.push(quote! {
|
||||
(#mapping_name, ::std::format!("#/components/schemas/{}", #schema_name))
|
||||
});
|
||||
|
||||
if let Some(discriminator_name) = &args.discriminator_name {
|
||||
create_schemas.push(quote! {
|
||||
{
|
||||
fn __check_is_object_type<T: #crate_name::types::IsObjectType>() {}
|
||||
__check_is_object_type::<#object_ty>();
|
||||
}
|
||||
|
||||
let schema = #crate_name::registry::MetaSchema {
|
||||
description: #description,
|
||||
all_of: ::std::vec![
|
||||
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(#crate_name::registry::MetaSchema {
|
||||
required: #required,
|
||||
properties: ::std::vec![
|
||||
(
|
||||
#discriminator_name,
|
||||
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(
|
||||
#crate_name::registry::MetaSchema {
|
||||
ty: "string",
|
||||
enum_items: ::std::vec![::std::convert::Into::into(#mapping_name)],
|
||||
example: ::std::option::Option::Some(::std::convert::Into::into(#mapping_name)),
|
||||
..#crate_name::registry::MetaSchema::ANY
|
||||
}
|
||||
)),
|
||||
)
|
||||
],
|
||||
..#crate_name::registry::MetaSchema::new("object")
|
||||
})),
|
||||
<#object_ty as #crate_name::types::Type>::schema_ref(),
|
||||
let schema = #crate_name::registry::MetaSchema {
|
||||
description: #description,
|
||||
all_of: ::std::vec![
|
||||
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(#crate_name::registry::MetaSchema {
|
||||
required: ::std::vec![#mapping_name],
|
||||
properties: ::std::vec![
|
||||
(
|
||||
#mapping_name,
|
||||
<#object_ty as #crate_name::types::Type>::schema_ref(),
|
||||
)
|
||||
],
|
||||
..#crate_name::registry::MetaSchema::ANY
|
||||
};
|
||||
..#crate_name::registry::MetaSchema::new("object")
|
||||
})),
|
||||
],
|
||||
..#crate_name::registry::MetaSchema::ANY
|
||||
};
|
||||
registry.schemas.insert(#schema_name, schema);
|
||||
});
|
||||
|
||||
registry.schemas.insert(#schema_name, schema);
|
||||
});
|
||||
|
||||
schemas.push(quote! {
|
||||
#crate_name::registry::MetaSchemaRef::Reference(#schema_name)
|
||||
});
|
||||
} else {
|
||||
schemas.push(quote! {
|
||||
<#object_ty as #crate_name::types::Type>::schema_ref()
|
||||
});
|
||||
schemas.push(quote! {
|
||||
#crate_name::registry::MetaSchemaRef::Reference(#schema_name)
|
||||
});
|
||||
} else if let Some(discriminator_name) = &args.discriminator_name {
|
||||
create_schemas.push(quote! {
|
||||
{
|
||||
fn __check_is_object_type<T: #crate_name::types::IsObjectType>() {}
|
||||
__check_is_object_type::<#object_ty>();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(
|
||||
Error::new_spanned(&variant.ident, "Incorrect oneof definition.").into(),
|
||||
);
|
||||
}
|
||||
|
||||
let schema = #crate_name::registry::MetaSchema {
|
||||
description: #description,
|
||||
all_of: ::std::vec![
|
||||
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(#crate_name::registry::MetaSchema {
|
||||
required: #required,
|
||||
properties: ::std::vec![
|
||||
(
|
||||
#discriminator_name,
|
||||
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(
|
||||
#crate_name::registry::MetaSchema {
|
||||
ty: "string",
|
||||
enum_items: ::std::vec![::std::convert::Into::into(#mapping_name)],
|
||||
example: ::std::option::Option::Some(::std::convert::Into::into(#mapping_name)),
|
||||
..#crate_name::registry::MetaSchema::ANY
|
||||
}
|
||||
)),
|
||||
)
|
||||
],
|
||||
..#crate_name::registry::MetaSchema::new("object")
|
||||
})),
|
||||
<#object_ty as #crate_name::types::Type>::schema_ref(),
|
||||
],
|
||||
..#crate_name::registry::MetaSchema::ANY
|
||||
};
|
||||
|
||||
registry.schemas.insert(#schema_name, schema);
|
||||
});
|
||||
|
||||
schemas.push(quote! {
|
||||
#crate_name::registry::MetaSchemaRef::Reference(#schema_name)
|
||||
});
|
||||
} else {
|
||||
schemas.push(quote! {
|
||||
<#object_ty as #crate_name::types::Type>::schema_ref()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +255,10 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
#(#from_json)*
|
||||
::std::result::Result::Err(#crate_name::types::ParseError::expected_type(value))
|
||||
},
|
||||
None if args.externally_tagged => quote! {
|
||||
#(#from_json)*
|
||||
::std::result::Result::Err(#crate_name::types::ParseError::expected_type(value))
|
||||
},
|
||||
// anyof
|
||||
None if !args.one_of => quote! {
|
||||
#(#from_json)*
|
||||
|
||||
@@ -5,6 +5,7 @@ Define an OpenAPI discriminator object.
|
||||
| Attribute | Description | Type | Optional |
|
||||
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|----------|
|
||||
| discriminator_name | The name of the property in the payload that will hold the discriminator value. | string | Y |
|
||||
| externally_tagged | Represent the union using the **externally tagged** format. The variant will be wrapped in an object where the key is the variant name. See [Serde enum representations](https://serde.rs/enum-representations.html#externally-tagged). | bool | Y |
|
||||
| one_of | Validates the value against exactly one of the subschemas | bool | Y |
|
||||
| external_docs | Specify a external resource for extended documentation | string | Y |
|
||||
| rename_all | Rename all the mapping name according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", "SCREAMING-KEBAB-CASE". | string | Y |
|
||||
|
||||
@@ -614,3 +614,233 @@ fn rename_all() {
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_externally_tagged() {
|
||||
#[derive(Object, Debug, PartialEq)]
|
||||
struct A {
|
||||
v1: i32,
|
||||
v2: String,
|
||||
}
|
||||
|
||||
#[derive(Object, Debug, PartialEq)]
|
||||
struct B {
|
||||
v3: bool,
|
||||
}
|
||||
|
||||
#[derive(Union, Debug, PartialEq)]
|
||||
#[oai(externally_tagged)]
|
||||
enum MyObj {
|
||||
A(A),
|
||||
B(B),
|
||||
}
|
||||
|
||||
let schema = get_meta::<MyObj>();
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
MetaSchema {
|
||||
rust_typename: Some("union::with_externally_tagged::MyObj"),
|
||||
ty: "object",
|
||||
any_of: vec![
|
||||
MetaSchemaRef::Reference("MyObj_A".to_string()),
|
||||
MetaSchemaRef::Reference("MyObj_B".to_string()),
|
||||
],
|
||||
..MetaSchema::ANY
|
||||
}
|
||||
);
|
||||
|
||||
let schema_myobj_a = get_meta_by_name::<MyObj>("MyObj_A");
|
||||
assert_eq!(
|
||||
schema_myobj_a,
|
||||
MetaSchema {
|
||||
all_of: vec![MetaSchemaRef::Inline(Box::new(MetaSchema {
|
||||
required: vec!["A"],
|
||||
properties: vec![("A", MetaSchemaRef::Reference("A".to_string()),)],
|
||||
..MetaSchema::new("object")
|
||||
})),],
|
||||
..MetaSchema::ANY
|
||||
}
|
||||
);
|
||||
|
||||
let schema_myobj_b = get_meta_by_name::<MyObj>("MyObj_B");
|
||||
assert_eq!(
|
||||
schema_myobj_b,
|
||||
MetaSchema {
|
||||
all_of: vec![MetaSchemaRef::Inline(Box::new(MetaSchema {
|
||||
required: vec!["B"],
|
||||
properties: vec![("B", MetaSchemaRef::Reference("B".to_string()),)],
|
||||
..MetaSchema::new("object")
|
||||
})),],
|
||||
..MetaSchema::ANY
|
||||
}
|
||||
);
|
||||
|
||||
let mut registry = Registry::new();
|
||||
MyObj::register(&mut registry);
|
||||
assert!(registry.schemas.contains_key("A"));
|
||||
assert!(registry.schemas.contains_key("B"));
|
||||
|
||||
assert_eq!(
|
||||
MyObj::parse_from_json(Some(json!({
|
||||
"A":{
|
||||
"v1": 100,
|
||||
"v2": "hello",
|
||||
}
|
||||
})))
|
||||
.unwrap(),
|
||||
MyObj::A(A {
|
||||
v1: 100,
|
||||
v2: "hello".to_string()
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MyObj::A(A {
|
||||
v1: 100,
|
||||
v2: "hello".to_string()
|
||||
})
|
||||
.to_json(),
|
||||
Some(json!({
|
||||
"A":{
|
||||
"v1": 100,
|
||||
"v2": "hello",
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MyObj::parse_from_json(Some(json!({
|
||||
"B": {
|
||||
"v3": true,
|
||||
}
|
||||
})))
|
||||
.unwrap(),
|
||||
MyObj::B(B { v3: true })
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MyObj::B(B { v3: true }).to_json(),
|
||||
Some(json!({
|
||||
"B": {
|
||||
"v3": true,
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_externally_tagged_mapping() {
|
||||
#[derive(Object, Debug, PartialEq)]
|
||||
struct A {
|
||||
v1: i32,
|
||||
v2: String,
|
||||
}
|
||||
|
||||
#[derive(Object, Debug, PartialEq)]
|
||||
struct B {
|
||||
v3: bool,
|
||||
}
|
||||
|
||||
#[derive(Union, Debug, PartialEq)]
|
||||
#[oai(externally_tagged)]
|
||||
enum MyObj {
|
||||
#[oai(mapping = "c")]
|
||||
A(A),
|
||||
#[oai(mapping = "d")]
|
||||
B(B),
|
||||
}
|
||||
|
||||
let schema = get_meta::<MyObj>();
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
MetaSchema {
|
||||
rust_typename: Some("union::with_externally_tagged_mapping::MyObj"),
|
||||
ty: "object",
|
||||
any_of: vec![
|
||||
MetaSchemaRef::Reference("MyObj_A".to_string()),
|
||||
MetaSchemaRef::Reference("MyObj_B".to_string()),
|
||||
],
|
||||
..MetaSchema::ANY
|
||||
}
|
||||
);
|
||||
|
||||
let schema_myobj_a = get_meta_by_name::<MyObj>("MyObj_A");
|
||||
assert_eq!(
|
||||
schema_myobj_a,
|
||||
MetaSchema {
|
||||
all_of: vec![MetaSchemaRef::Inline(Box::new(MetaSchema {
|
||||
required: vec!["c"],
|
||||
properties: vec![("c", MetaSchemaRef::Reference("A".to_string()),)],
|
||||
..MetaSchema::new("object")
|
||||
})),],
|
||||
..MetaSchema::ANY
|
||||
}
|
||||
);
|
||||
|
||||
let schema_myobj_b = get_meta_by_name::<MyObj>("MyObj_B");
|
||||
assert_eq!(
|
||||
schema_myobj_b,
|
||||
MetaSchema {
|
||||
all_of: vec![MetaSchemaRef::Inline(Box::new(MetaSchema {
|
||||
required: vec!["d"],
|
||||
properties: vec![("d", MetaSchemaRef::Reference("B".to_string()),)],
|
||||
..MetaSchema::new("object")
|
||||
})),],
|
||||
..MetaSchema::ANY
|
||||
}
|
||||
);
|
||||
|
||||
let mut registry = Registry::new();
|
||||
MyObj::register(&mut registry);
|
||||
assert!(registry.schemas.contains_key("A"));
|
||||
assert!(registry.schemas.contains_key("B"));
|
||||
|
||||
assert_eq!(
|
||||
MyObj::parse_from_json(Some(json!({
|
||||
"c":{
|
||||
"v1": 100,
|
||||
"v2": "hello",
|
||||
}
|
||||
})))
|
||||
.unwrap(),
|
||||
MyObj::A(A {
|
||||
v1: 100,
|
||||
v2: "hello".to_string()
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MyObj::A(A {
|
||||
v1: 100,
|
||||
v2: "hello".to_string()
|
||||
})
|
||||
.to_json(),
|
||||
Some(json!({
|
||||
"c":{
|
||||
"v1": 100,
|
||||
"v2": "hello",
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MyObj::parse_from_json(Some(json!({
|
||||
"d": {
|
||||
"v3": true,
|
||||
}
|
||||
})))
|
||||
.unwrap(),
|
||||
MyObj::B(B { v3: true })
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MyObj::B(B { v3: true }).to_json(),
|
||||
Some(json!({
|
||||
"d": {
|
||||
"v3": true,
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user