Skip to content

Commit b88e3b8

Browse files
authored
feat(poem-openapi) Allow childless enum variant (#1159)
* parse_from_json and to_json * Added schema + mappers * cleanup * fmt * Apply suggestion from @Nahuel-M typo
1 parent d9c88f9 commit b88e3b8

File tree

2 files changed

+280
-125
lines changed

2 files changed

+280
-125
lines changed

poem-openapi-derive/src/union.rs

Lines changed: 207 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
5858
let discriminator_name = &args.discriminator_name;
5959

6060
let Data::Enum(e) = &args.data else {
61-
return Err(Error::new_spanned(ident, "AnyOf can only be applied to an enum.").into());
61+
return Err(
62+
Error::new_spanned(ident, "AnyOf (Union) can only be applied to an enum.").into(),
63+
);
6264
};
6365

6466
if discriminator_name.is_some() && args.externally_tagged {
@@ -85,122 +87,45 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
8587
for variant in e {
8688
let item_ident = &variant.ident;
8789

88-
if variant.fields.len() != 1 {
89-
return Err(Error::new_spanned(&variant.ident, "Incorrect oneof definition.").into());
90-
}
91-
92-
let object_ty = &variant.fields.fields[0];
93-
let format_string = format!("{{}}_{}", item_ident);
94-
let schema_name = quote! {
95-
::std::format!(#format_string, <Self as #crate_name::types::Type>::name())
96-
};
9790
let mapping_name = match &variant.mapping {
9891
Some(mapping) => mapping.clone(),
9992
None => apply_rename_rule_variant(args.rename_all, item_ident.unraw().to_string()),
10093
};
101-
types.push(object_ty);
102-
103-
if args.externally_tagged {
104-
from_json.push(quote! {
105-
if let value @ ::std::option::Option::Some(_) = value.as_object().and_then(|obj| obj.get(#mapping_name)).cloned() {
106-
return <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(value)
107-
.map(Self::#item_ident)
108-
.map_err(#crate_name::types::ParseError::propagate);
109-
}
110-
});
111-
} else if discriminator_name.is_some() {
112-
from_json.push(quote! {
113-
if ::std::matches!(discriminator_name, ::std::option::Option::Some(discriminator_name) if discriminator_name == #mapping_name) {
114-
return <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(::std::option::Option::Some(value))
115-
.map(Self::#item_ident)
116-
.map_err(#crate_name::types::ParseError::propagate);
117-
}
118-
});
119-
} else if !args.one_of {
120-
// any of
121-
from_json.push(quote! {
122-
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)))
123-
.map(Self::#item_ident) {
124-
return ::std::result::Result::Ok(obj);
125-
}
126-
});
127-
} else {
128-
// one of
129-
from_json.push(quote! {
130-
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)))
131-
.map(Self::#item_ident) {
132-
if res_obj.is_some() {
133-
return ::std::result::Result::Err(#crate_name::types::ParseError::expected_type(value));
134-
}
135-
res_obj = Some(obj);
136-
}
137-
});
138-
}
13994

140-
if args.externally_tagged {
141-
to_json.push(quote! {
142-
Self::#item_ident(obj) => {
143-
let value = <#object_ty as #crate_name::types::ToJSON>::to_json(obj);
144-
let mut wrapped = #crate_name::__private::serde_json::Map::new();
145-
wrapped.insert(::std::convert::Into::into(#mapping_name), ::std::option::Option::unwrap_or_default(value));
146-
::std::option::Option::Some(#crate_name::__private::serde_json::Value::Object(wrapped))
147-
}
148-
});
149-
} else if let Some(discriminator_name) = &discriminator_name {
150-
to_json.push(quote! {
151-
Self::#item_ident(obj) => {
152-
let mut value = <#object_ty as #crate_name::types::ToJSON>::to_json(obj);
153-
if let ::std::option::Option::Some(obj) = value.as_mut().and_then(|value| value.as_object_mut()) {
154-
obj.insert(::std::convert::Into::into(#discriminator_name), ::std::convert::Into::into(#mapping_name));
155-
}
156-
value
157-
}
158-
});
159-
} else {
160-
to_json.push(quote! {
161-
Self::#item_ident(obj) => <#object_ty as #crate_name::types::ToJSON>::to_json(obj)
162-
});
163-
}
164-
165-
mapping.push(quote! {
166-
(::std::string::ToString::to_string(#mapping_name), ::std::format!("#/components/schemas/{}", #schema_name))
167-
});
168-
169-
if args.externally_tagged {
170-
create_schemas.push(quote! {
171-
let schema = #crate_name::registry::MetaSchema {
172-
description: #description,
173-
all_of: ::std::vec![
174-
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(#crate_name::registry::MetaSchema {
175-
required: ::std::vec![#mapping_name],
176-
properties: ::std::vec![
177-
(
178-
#mapping_name,
179-
<#object_ty as #crate_name::types::Type>::schema_ref(),
180-
)
181-
],
182-
..#crate_name::registry::MetaSchema::new("object")
183-
})),
184-
],
185-
..#crate_name::registry::MetaSchema::ANY
186-
};
187-
registry.schemas.insert(#schema_name, schema);
188-
});
189-
190-
schemas.push(quote! {
191-
#crate_name::registry::MetaSchemaRef::Reference(#schema_name)
192-
});
193-
} else if let Some(discriminator_name) = &args.discriminator_name {
194-
create_schemas.push(quote! {
195-
{
196-
fn __check_is_object_type<T: #crate_name::types::IsObjectType>() {}
197-
__check_is_object_type::<#object_ty>();
198-
}
199-
200-
let schema = #crate_name::registry::MetaSchema {
201-
description: #description,
202-
all_of: ::std::vec![
203-
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(#crate_name::registry::MetaSchema {
95+
match variant.fields.len() {
96+
0 => {
97+
if args.externally_tagged {
98+
return Err(Error::new_spanned(
99+
&variant.ident,
100+
"Empty variant cannot be externally tagged.",
101+
)
102+
.into());
103+
} else if let Some(discriminator_name) = &discriminator_name {
104+
from_json.push(quote! {
105+
if ::std::matches!(discriminator_name, ::std::option::Option::Some(discriminator_name) if discriminator_name == #mapping_name) {
106+
return ::std::result::Result::Ok(Self::#item_ident)
107+
}
108+
});
109+
to_json.push(quote! {
110+
Self::#item_ident => {
111+
::std::option::Option::Some(#crate_name::__private::serde_json::json!({ #discriminator_name: #mapping_name }))
112+
}
113+
});
114+
115+
// Create a named schema for the childless variant to support discriminator
116+
// mapping
117+
let format_string = format!("{{}}_{}", item_ident);
118+
let schema_name = quote! {
119+
::std::format!(#format_string, <Self as #crate_name::types::Type>::name())
120+
};
121+
122+
mapping.push(quote! {
123+
(::std::string::ToString::to_string(#mapping_name), ::std::format!("#/components/schemas/{}", #schema_name))
124+
});
125+
126+
create_schemas.push(quote! {
127+
let schema = #crate_name::registry::MetaSchema {
128+
description: #description,
204129
required: #required,
205130
properties: ::std::vec![
206131
(
@@ -216,22 +141,179 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
216141
)
217142
],
218143
..#crate_name::registry::MetaSchema::new("object")
219-
})),
220-
<#object_ty as #crate_name::types::Type>::schema_ref(),
221-
],
222-
..#crate_name::registry::MetaSchema::ANY
144+
};
145+
registry.schemas.insert(#schema_name, schema);
146+
});
147+
148+
schemas.push(quote! {
149+
#crate_name::registry::MetaSchemaRef::Reference(#schema_name)
150+
});
151+
} else {
152+
from_json.push(quote! {
153+
if value.is_object(){ return ::std::result::Result::Ok(Self::#item_ident); }
154+
});
155+
to_json.push(quote! {
156+
Self::#item_ident => ::std::option::Option::Some(#crate_name::__private::serde_json::json!({}))
157+
});
158+
159+
// For childless variants without a discriminator, create an inline empty object
160+
// schema
161+
schemas.push(quote! {
162+
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(
163+
#crate_name::registry::MetaSchema::new("object")
164+
))
165+
});
166+
}
167+
}
168+
1 => {
169+
let object_ty = &variant.fields.fields[0];
170+
let format_string = format!("{{}}_{}", item_ident);
171+
let schema_name = quote! {
172+
::std::format!(#format_string, <Self as #crate_name::types::Type>::name())
223173
};
174+
types.push(object_ty);
175+
176+
if args.externally_tagged {
177+
from_json.push(quote! {
178+
if let value @ ::std::option::Option::Some(_) = value.as_object().and_then(|obj| obj.get(#mapping_name)).cloned() {
179+
return <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(value)
180+
.map(Self::#item_ident)
181+
.map_err(#crate_name::types::ParseError::propagate);
182+
}
183+
});
184+
} else if discriminator_name.is_some() {
185+
from_json.push(quote! {
186+
if ::std::matches!(discriminator_name, ::std::option::Option::Some(discriminator_name) if discriminator_name == #mapping_name) {
187+
return <#object_ty as #crate_name::types::ParseFromJSON>::parse_from_json(::std::option::Option::Some(value))
188+
.map(Self::#item_ident)
189+
.map_err(#crate_name::types::ParseError::propagate);
190+
}
191+
});
192+
} else if !args.one_of {
193+
// any of
194+
from_json.push(quote! {
195+
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)))
196+
.map(Self::#item_ident) {
197+
return ::std::result::Result::Ok(obj);
198+
}
199+
});
200+
} else {
201+
// one of
202+
from_json.push(quote! {
203+
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)))
204+
.map(Self::#item_ident) {
205+
if res_obj.is_some() {
206+
return ::std::result::Result::Err(#crate_name::types::ParseError::expected_type(value));
207+
}
208+
res_obj = Some(obj);
209+
}
210+
});
211+
}
212+
213+
if args.externally_tagged {
214+
to_json.push(quote! {
215+
Self::#item_ident(obj) => {
216+
let value = <#object_ty as #crate_name::types::ToJSON>::to_json(obj);
217+
let mut wrapped = #crate_name::__private::serde_json::Map::new();
218+
wrapped.insert(::std::convert::Into::into(#mapping_name), ::std::option::Option::unwrap_or_default(value));
219+
::std::option::Option::Some(#crate_name::__private::serde_json::Value::Object(wrapped))
220+
}
221+
});
222+
} else if let Some(discriminator_name) = &discriminator_name {
223+
to_json.push(quote! {
224+
Self::#item_ident(obj) => {
225+
let mut value = <#object_ty as #crate_name::types::ToJSON>::to_json(obj);
226+
if let ::std::option::Option::Some(obj) = value.as_mut().and_then(|value| value.as_object_mut()) {
227+
obj.insert(::std::convert::Into::into(#discriminator_name), ::std::convert::Into::into(#mapping_name));
228+
}
229+
value
230+
}
231+
});
232+
} else {
233+
to_json.push(quote! {
234+
Self::#item_ident(obj) => <#object_ty as #crate_name::types::ToJSON>::to_json(obj)
235+
});
236+
}
237+
238+
mapping.push(quote! {
239+
(::std::string::ToString::to_string(#mapping_name), ::std::format!("#/components/schemas/{}", #schema_name))
240+
});
241+
242+
if args.externally_tagged {
243+
create_schemas.push(quote! {
244+
let schema = #crate_name::registry::MetaSchema {
245+
description: #description,
246+
all_of: ::std::vec![
247+
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(#crate_name::registry::MetaSchema {
248+
required: ::std::vec![#mapping_name],
249+
properties: ::std::vec![
250+
(
251+
#mapping_name,
252+
<#object_ty as #crate_name::types::Type>::schema_ref(),
253+
)
254+
],
255+
..#crate_name::registry::MetaSchema::new("object")
256+
})),
257+
],
258+
..#crate_name::registry::MetaSchema::ANY
259+
};
260+
registry.schemas.insert(#schema_name, schema);
261+
});
262+
263+
schemas.push(quote! {
264+
#crate_name::registry::MetaSchemaRef::Reference(#schema_name)
265+
});
266+
} else if let Some(discriminator_name) = &args.discriminator_name {
267+
create_schemas.push(quote! {
268+
{
269+
fn __check_is_object_type<T: #crate_name::types::IsObjectType>() {}
270+
__check_is_object_type::<#object_ty>();
271+
}
224272

225-
registry.schemas.insert(#schema_name, schema);
226-
});
273+
let schema = #crate_name::registry::MetaSchema {
274+
description: #description,
275+
all_of: ::std::vec![
276+
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(#crate_name::registry::MetaSchema {
277+
required: #required,
278+
properties: ::std::vec![
279+
(
280+
#discriminator_name,
281+
#crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(
282+
#crate_name::registry::MetaSchema {
283+
ty: "string",
284+
enum_items: ::std::vec![::std::convert::Into::into(#mapping_name)],
285+
example: ::std::option::Option::Some(::std::convert::Into::into(#mapping_name)),
286+
..#crate_name::registry::MetaSchema::ANY
287+
}
288+
)),
289+
)
290+
],
291+
..#crate_name::registry::MetaSchema::new("object")
292+
})),
293+
<#object_ty as #crate_name::types::Type>::schema_ref(),
294+
],
295+
..#crate_name::registry::MetaSchema::ANY
296+
};
297+
298+
registry.schemas.insert(#schema_name, schema);
299+
});
227300

228-
schemas.push(quote! {
229-
#crate_name::registry::MetaSchemaRef::Reference(#schema_name)
230-
});
231-
} else {
232-
schemas.push(quote! {
233-
<#object_ty as #crate_name::types::Type>::schema_ref()
234-
});
301+
schemas.push(quote! {
302+
#crate_name::registry::MetaSchemaRef::Reference(#schema_name)
303+
});
304+
} else {
305+
schemas.push(quote! {
306+
<#object_ty as #crate_name::types::Type>::schema_ref()
307+
});
308+
}
309+
}
310+
2.. => {
311+
return Err(Error::new_spanned(
312+
&variant.ident,
313+
"Oneof (Union) does not support multiple variant fields",
314+
)
315+
.into());
316+
}
235317
}
236318
}
237319

0 commit comments

Comments
 (0)