use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields};
const HELPER: &str = "openapi_schema";
pub fn derive_openapi_schema(input: TokenStream) -> TokenStream {
let input = match syn::parse2::<DeriveInput>(input) {
Ok(input) => input,
Err(e) => return e.to_compile_error(),
};
if let Err(err) = reject_misplaced_helper(&input) {
return err;
}
let crate_override = match helper_crate_override(&input.attrs) {
Ok(name) => name,
Err(err) => return err,
};
let _guard = autumn_macros_support::crate_path::set_target(crate_override.as_deref());
autumn_macros_support::crate_path::finalize(expand(&input))
}
fn helper_crate_override(attrs: &[syn::Attribute]) -> Result<Option<String>, TokenStream> {
let mut found: Option<String> = None;
for attr in attrs.iter().filter(|a| a.path().is_ident(HELPER)) {
let syn::Meta::List(list) = &attr.meta else {
return Err(helper_error(attr));
};
let (name, rest) =
autumn_macros_support::crate_path::extract_crate_override(list.tokens.clone())?;
if name.is_none() || !rest.is_empty() || found.is_some() {
return Err(helper_error(attr));
}
found = name;
}
Ok(found)
}
fn reject_misplaced_helper(input: &DeriveInput) -> Result<(), TokenStream> {
let nested: Vec<&syn::Attribute> = match &input.data {
Data::Struct(d) => d.fields.iter().flat_map(|f| &f.attrs).collect(),
Data::Enum(d) => d
.variants
.iter()
.flat_map(|v| v.attrs.iter().chain(v.fields.iter().flat_map(|f| &f.attrs)))
.collect(),
Data::Union(d) => d.fields.named.iter().flat_map(|f| &f.attrs).collect(),
};
if let Some(attr) = nested.iter().find(|a| a.path().is_ident(HELPER)) {
return Err(syn::Error::new_spanned(
attr,
"`#[openapi_schema]` is valid on the struct or enum only",
)
.to_compile_error());
}
Ok(())
}
fn helper_error(attr: &syn::Attribute) -> TokenStream {
syn::Error::new_spanned(
attr,
"expected one `#[openapi_schema(crate = \"...\")]` attribute",
)
.to_compile_error()
}
fn expand(input: &DeriveInput) -> TokenStream {
let name = &input.ident;
if !input.generics.params.is_empty() {
return syn::Error::new_spanned(
&input.generics,
"#[derive(OpenApiSchema)] does not support generic types",
)
.to_compile_error();
}
let schema_body = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(named) => {
if let Err(e) = reject_undescribable_struct(input, named) {
return e.to_compile_error();
}
let field_refs: Vec<&syn::Field> = named
.named
.iter()
.filter(|f| {
autumn_macros_support::schema::serde_bare_word(&f.attrs, &["skip"])
.is_none()
})
.collect();
let field_ref_refs: Vec<&&syn::Field> = field_refs.iter().collect();
let rename_all_rule =
autumn_macros_support::schema::serde_rename_all_serialize_rule(&input.attrs);
let container_default =
autumn_macros_support::schema::has_serde_default(&input.attrs);
let body = autumn_macros_support::schema::emit_schema_fn_body_full(
&field_ref_refs,
container_default,
&[],
rename_all_rule.as_deref(),
&|f: &syn::Field| autumn_macros_support::schema::has_serde_default(&f.attrs),
);
if autumn_macros_support::schema::serde_bare_word(
&input.attrs,
&["deny_unknown_fields"],
)
.is_some()
{
quote! {{
let mut __autumn_closed = { #body };
if let Some(__autumn_obj) = __autumn_closed.as_object_mut() {
__autumn_obj.insert(
"additionalProperties".to_owned(),
::autumn_web::reexports::serde_json::json!(false),
);
}
__autumn_closed
}}
} else {
body
}
}
_ => {
return syn::Error::new_spanned(
input,
"#[derive(OpenApiSchema)] is only supported on structs with named fields",
)
.to_compile_error();
}
},
Data::Enum(data) => match enum_schema_body(input, data) {
Ok(body) => body,
Err(e) => return e.to_compile_error(),
},
Data::Union(_) => {
return syn::Error::new_spanned(
input,
"#[derive(OpenApiSchema)] is only supported on structs and enums",
)
.to_compile_error();
}
};
quote! {
impl ::autumn_web::openapi::OpenApiSchema for #name {
fn schema_name() -> &'static str {
::core::stringify!(#name)
}
fn schema() -> ::autumn_web::reexports::serde_json::Value {
#schema_body
}
}
::autumn_web::reexports::inventory::submit! {
::autumn_web::openapi::DerivedSchemaDescriptor {
name: ::core::stringify!(#name),
identity: ::autumn_web::openapi::type_name_of::<#name>,
schema: <#name as ::autumn_web::openapi::OpenApiSchema>::schema,
}
}
}
}
fn enum_schema_body(
input: &DeriveInput,
data: &syn::DataEnum,
) -> syn::Result<proc_macro2::TokenStream> {
reject_undescribable_enum(input, data)?;
let rename_all_rule =
autumn_macros_support::schema::serde_rename_all_serialize_rule(&input.attrs);
let values: Vec<String> = data
.variants
.iter()
.filter(|v| !autumn_macros_support::schema::variant_is_serde_skipped(v))
.map(|v| {
let raw = v.ident.to_string();
let raw = raw.strip_prefix("r#").unwrap_or(&raw).to_owned();
autumn_macros_support::schema::variant_serde_serialize_rename(v)
.or_else(|| {
rename_all_rule.as_deref().and_then(|rule| {
autumn_macros_support::schema::apply_serde_rename_all_rule_to_variant(
rule, &raw,
)
})
})
.unwrap_or(raw)
})
.collect();
if values.is_empty() {
return Err(syn::Error::new_spanned(
input,
"#[derive(OpenApiSchema)] needs at least one non-skipped variant to advertise",
));
}
Ok(quote! {
::autumn_web::reexports::serde_json::json!({
"type": "string",
"enum": [#(#values),*],
})
})
}
fn reject_undescribable_conditional_skip(
field: &syn::Field,
container_attrs: &[syn::Attribute],
) -> syn::Result<()> {
if autumn_macros_support::schema::field_has_skip_serializing_if(field)
&& !autumn_macros_support::schema::has_serde_default(&field.attrs)
&& !autumn_macros_support::schema::has_serde_default(container_attrs)
{
return Err(syn::Error::new_spanned(
field,
"#[derive(OpenApiSchema)] cannot describe a field with \
`#[serde(skip_serializing_if = ...)]` and no `#[serde(default)]`: that \
attribute governs serialization only, so a response may omit the field \
while serde still rejects a request that does. Being spelled `Option<T>` \
is not enough — this macro sees only the tokens, and a type of your own \
named `Option` reads the same but is required on the way in. Add \
`#[serde(default)]` (a no-op for a real `Option<T>`, which serde already \
fills with `None`), or write the `OpenApiSchema` impl by hand and register \
it with `OpenApiConfig::register_schema`.",
));
}
Ok(())
}
fn reject_undescribable_struct(input: &DeriveInput, named: &syn::FieldsNamed) -> syn::Result<()> {
if let Some(word) =
autumn_macros_support::schema::serde_bare_word(&input.attrs, &["transparent", "untagged"])
{
return Err(syn::Error::new_spanned(
input,
format!(
"#[derive(OpenApiSchema)] cannot describe `#[serde({word})]`: serde does not \
put an object with these fields on the wire, so the derived schema would \
advertise a shape the handler neither accepts nor returns. Write the \
`OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`."
),
));
}
if let Some(key) = autumn_macros_support::schema::serde_valued_key(
&input.attrs,
&["into", "from", "try_from", "tag"],
) {
return Err(syn::Error::new_spanned(
input,
format!(
"#[derive(OpenApiSchema)] cannot describe `#[serde({key} = ...)]`: it re-shapes \
what reaches the wire, so an object schema built from these fields would be \
wrong. Write the `OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`."
),
));
}
if let Some(key) = autumn_macros_support::schema::serde_split_rename(&input.attrs, "rename_all")
{
return Err(syn::Error::new_spanned(input, split_rename_message(key)));
}
for field in &named.named {
if let Some(word) =
autumn_macros_support::schema::serde_bare_word(&field.attrs, &["flatten"])
{
return Err(syn::Error::new_spanned(
field,
format!(
"#[derive(OpenApiSchema)] cannot describe `#[serde({word})]`: serde merges \
this field's keys into the containing object, while the derived schema \
would publish it as a nested property — a generated client would send a \
nesting the handler does not accept and expect one the server never \
emits. Write the `OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`."
),
));
}
if let Some(key) = autumn_macros_support::schema::serde_valued_key(
&field.attrs,
&["with", "serialize_with", "deserialize_with"],
) {
return Err(syn::Error::new_spanned(
field,
format!(
"#[derive(OpenApiSchema)] cannot describe a field with \
`#[serde({key} = ...)]`: the adapter decides what actually reaches the \
wire, so a schema built from the field's Rust type would advertise a \
shape serde neither writes nor accepts. Write the `OpenApiSchema` impl \
by hand and register it with `OpenApiConfig::register_schema`."
),
));
}
if let Some(word) = autumn_macros_support::schema::variant_directional_skip_on_field(field)
{
return Err(syn::Error::new_spanned(
field,
format!(
"#[derive(OpenApiSchema)] cannot describe a field skipped in only one serde \
direction (`#[serde({word})]`): one schema covers both requests and \
responses. Use `#[serde(skip)]` if the field should not appear at all, or \
write the `OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`."
),
));
}
reject_undescribable_conditional_skip(field, &input.attrs)?;
if autumn_macros_support::schema::has_serde_alias(&field.attrs) {
return Err(syn::Error::new_spanned(
field,
"#[derive(OpenApiSchema)] cannot describe a field with `#[serde(alias = \"…\")]`: \
the alias is accepted when deserializing but never written when serializing, so \
one property name cannot be right for both requests and responses — a validator \
would reject a request the handler accepts. Use `#[serde(rename = \"…\")]` if the \
wire name should change in both directions, or write the `OpenApiSchema` impl by \
hand and register it with `OpenApiConfig::register_schema`.",
));
}
if let Some(key) = autumn_macros_support::schema::serde_split_rename(&field.attrs, "rename")
{
return Err(syn::Error::new_spanned(field, split_rename_message(key)));
}
}
Ok(())
}
fn split_rename_message(key: &str) -> String {
format!(
"#[derive(OpenApiSchema)] cannot describe a split `#[serde({key}(serialize = ..., \
deserialize = ...))]` whose two sides differ: one schema is advertised for both \
requests and responses, so a client generated from the serialize spelling would send \
a value the handler's `Deserialize` rejects. Use a symmetric `{key} = \"...\"`, or \
write the `OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`."
)
}
fn reject_untagged_variants(data: &syn::DataEnum) -> syn::Result<()> {
if let Some(variant) = data
.variants
.iter()
.find(|v| autumn_macros_support::schema::serde_bare_word(&v.attrs, &["untagged"]).is_some())
{
return Err(syn::Error::new_spanned(
variant,
"#[derive(OpenApiSchema)] cannot describe a variant marked `#[serde(untagged)]`: \
serde writes it as `null` rather than as its name, so the closed string set the \
derive would publish is wrong for this variant and a client decodes the wrong \
type. Write the `OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`.",
));
}
Ok(())
}
fn reject_aliased_variants(data: &syn::DataEnum) -> syn::Result<()> {
if let Some(variant) = data
.variants
.iter()
.find(|v| autumn_macros_support::schema::serde_bare_word(&v.attrs, &["other"]).is_some())
{
return Err(syn::Error::new_spanned(
variant,
"#[derive(OpenApiSchema)] cannot describe a variant marked \
`#[serde(other)]`: it is serde's catch-all, so deserialization accepts any \
unrecognised spelling while serialization only ever writes this variant's own \
name — a closed string set would reject requests the handler accepts. Write the \
`OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`.",
));
}
if let Some(variant) = data
.variants
.iter()
.find(|v| autumn_macros_support::schema::variant_has_serde_alias(v))
{
return Err(syn::Error::new_spanned(
variant,
"#[derive(OpenApiSchema)] cannot describe a variant with `#[serde(alias = \"…\")]`: \
the alias is accepted when deserializing but never written when serializing, so one \
string set cannot be right for both requests and responses — listing it advertises \
a value the server never emits, omitting it rejects one the handler accepts. Use \
`#[serde(rename = \"…\")]` if the wire spelling should change in both directions, \
or write the `OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`.",
));
}
Ok(())
}
fn reject_undescribable_enum(input: &DeriveInput, data: &syn::DataEnum) -> syn::Result<()> {
if let Some(variant) = data
.variants
.iter()
.find(|v| !matches!(v.fields, Fields::Unit))
{
return Err(syn::Error::new_spanned(
variant,
"#[derive(OpenApiSchema)] supports only enums whose variants are all unit variants \
(they map to a JSON string enum). For a data-carrying enum, write the \
`OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`.",
));
}
if let Some(repr) = autumn_macros_support::schema::serde_enum_representation(&input.attrs) {
let (written, becomes) = match repr {
"untagged" => ("#[serde(untagged)]", "`null`"),
"tag" => ("#[serde(tag = \"...\")]", "an object"),
"content" => ("#[serde(content = \"...\")]", "an object"),
"into" => (
"#[serde(into = \"...\")]",
"whatever the conversion type serializes as",
),
"from" => (
"#[serde(from = \"...\")]",
"whatever the conversion type serializes as",
),
_ => (
"#[serde(try_from = \"...\")]",
"whatever the conversion type serializes as",
),
};
return Err(syn::Error::new_spanned(
input,
format!(
"#[derive(OpenApiSchema)] supports only serde's default (externally tagged) \
enum representation; `{written}` makes a unit variant serialize as {becomes}, \
not the JSON string the derived schema would advertise — a generated client \
would send a contract the handler does not accept. Write the `OpenApiSchema` \
impl by hand and register it with `OpenApiConfig::register_schema`."
),
));
}
if let Some(variant) = data
.variants
.iter()
.find(|v| autumn_macros_support::schema::variant_directional_skip(v).is_some())
{
let attribute = autumn_macros_support::schema::variant_directional_skip(variant)
.expect("the find predicate just matched it");
let consequence = if attribute == "skip_deserializing" {
"it is still serialized, so advertising it would tell a client it may send a \
value serde rejects as an unknown variant"
} else {
"it is still accepted on input, so omitting it would deny a value the handler takes"
};
return Err(syn::Error::new_spanned(
variant,
format!(
"#[derive(OpenApiSchema)] cannot describe a variant skipped in only one serde \
direction: one schema covers both requests and responses, and with \
`#[serde({attribute})]` {consequence}. Use `#[serde(skip)]` if the variant \
should not appear at all, or write the `OpenApiSchema` impl by hand and \
register it with `OpenApiConfig::register_schema`."
),
));
}
reject_aliased_variants(data)?;
reject_untagged_variants(data)?;
let split = autumn_macros_support::schema::serde_split_rename(&input.attrs, "rename_all")
.map(|key| (key, None))
.or_else(|| {
data.variants.iter().find_map(|v| {
autumn_macros_support::schema::serde_split_rename(&v.attrs, "rename")
.map(|key| (key, Some(v)))
})
});
if let Some((key, variant)) = split {
let message = format!(
"#[derive(OpenApiSchema)] cannot describe a split `#[serde({key}(serialize = ..., \
deserialize = ...))]` whose two sides differ: one schema is advertised for both \
requests and responses, so a client generated from the serialize spelling would \
send a value the handler's `Deserialize` rejects. Use a symmetric \
`{key} = \"...\"`, or write the `OpenApiSchema` impl by hand and register it with \
`OpenApiConfig::register_schema`."
);
return Err(variant.map_or_else(
|| syn::Error::new_spanned(input, message.clone()),
|v| syn::Error::new_spanned(v, message.clone()),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
fn derive_str(input: TokenStream) -> String {
derive_openapi_schema(input).to_string()
}
#[test]
fn crate_override_roots_the_impl_and_inventory_at_the_named_crate() {
let out = derive_str(quote! {
#[openapi_schema(crate = "web_new")]
struct Params { q: String }
});
assert!(
out.contains(":: web_new :: openapi :: OpenApiSchema"),
"{out}"
);
assert!(
out.contains(":: web_new :: openapi :: DerivedSchemaDescriptor"),
"{out}"
);
assert!(!out.contains(":: autumn_web"), "leaked default path: {out}");
}
#[test]
fn crate_override_applies_to_enums() {
let out = derive_str(quote! {
#[openapi_schema(crate = "web_new")]
enum Status { Open, Closed }
});
assert!(
out.contains(":: web_new :: openapi :: OpenApiSchema"),
"{out}"
);
assert!(!out.contains(":: autumn_web"), "{out}");
}
#[test]
fn no_override_keeps_the_default_root() {
let out = derive_str(quote! { struct Params { q: String } });
assert!(
out.contains(":: autumn_web :: openapi :: OpenApiSchema"),
"{out}"
);
}
#[test]
fn crate_override_does_not_leak_to_the_next_expansion() {
let _ = derive_str(quote! {
#[openapi_schema(crate = "web_new")]
struct A { q: String }
});
assert_eq!(
autumn_macros_support::crate_path::current_target(),
"autumn_web"
);
}
fn err_of(input: TokenStream) -> String {
let out = derive_str(input);
assert!(out.contains("compile_error"), "expected an error: {out}");
out
}
fn helper_err_of(input: TokenStream) {
let out = err_of(input);
assert!(out.contains("expected one"), "{out}");
}
#[test]
fn a_non_string_override_is_refused() {
err_of(quote! {
#[openapi_schema(crate = web_new)]
struct A { q: String }
});
}
#[test]
fn an_invalid_identifier_override_is_refused() {
err_of(quote! {
#[openapi_schema(crate = "not an ident")]
struct A { q: String }
});
}
#[test]
fn an_unknown_helper_argument_is_refused() {
helper_err_of(quote! {
#[openapi_schema(krate = "web_new")]
struct A { q: String }
});
}
#[test]
fn an_extra_argument_after_the_override_is_refused() {
helper_err_of(quote! {
#[openapi_schema(crate = "a", foo)]
struct A { q: String }
});
}
#[test]
fn a_crate_key_without_a_value_is_refused() {
err_of(quote! {
#[openapi_schema(crate)]
struct A { q: String }
});
}
#[test]
fn an_override_on_a_tuple_struct_still_fails_for_the_shape() {
let out = err_of(quote! {
#[openapi_schema(crate = "web_new")]
struct A(String);
});
assert!(out.contains("named fields"), "{out}");
}
#[test]
fn the_helper_on_a_field_is_refused() {
let out = err_of(quote! {
struct A { #[openapi_schema(crate = "web_new")] q: String }
});
assert!(out.contains("struct or enum only"), "{out}");
}
#[test]
fn the_helper_on_a_variant_is_refused() {
let out = err_of(quote! {
enum A { #[openapi_schema(crate = "web_new")] Open }
});
assert!(out.contains("struct or enum only"), "{out}");
}
#[test]
fn the_helper_on_a_union_field_is_refused() {
let out = err_of(quote! {
union U { #[openapi_schema(crate = "web_new")] a: u32, b: f32 }
});
assert!(out.contains("struct or enum only"), "{out}");
}
#[test]
fn a_union_is_refused_for_its_shape() {
let out = err_of(quote! { union U { a: u32, b: f32 } });
assert!(out.contains("structs and enums"), "{out}");
}
#[test]
fn a_generic_type_is_refused_with_an_override() {
let out = err_of(quote! {
#[openapi_schema(crate = "web_new")]
struct G<T> { q: T }
});
assert!(out.contains("generic"), "{out}");
}
#[test]
fn input_that_is_not_an_item_is_refused() {
let out = derive_str(quote! { not an item });
assert!(out.contains("compile_error"), "{out}");
}
#[test]
fn the_helper_on_a_tuple_field_is_refused() {
let out = err_of(quote! {
struct T(#[openapi_schema(crate = "web_new")] String);
});
assert!(out.contains("struct or enum only"), "{out}");
}
#[test]
fn a_bare_helper_attribute_is_refused() {
helper_err_of(quote! {
#[openapi_schema]
struct A { q: String }
});
}
#[test]
fn a_duplicate_override_is_refused() {
helper_err_of(quote! {
#[openapi_schema(crate = "a")]
#[openapi_schema(crate = "b")]
struct A { q: String }
});
}
fn audit(input: &DeriveInput) -> Result<(), String> {
let Data::Struct(data) = &input.data else {
panic!("fixture must be a struct");
};
let Fields::Named(named) = &data.fields else {
panic!("fixture must have named fields");
};
reject_undescribable_struct(input, named).map_err(|e| e.to_string())
}
#[test]
fn a_field_serialize_adapter_is_refused() {
let err = audit(&parse_quote! {
struct Money {
#[serde(serialize_with = "as_string")]
amount: i64,
}
})
.expect_err("a serialize adapter must be refused");
assert!(
err.contains("serialize_with"),
"the message must name the attribute it refused: {err}"
);
}
#[test]
fn a_field_with_adapter_is_refused() {
let err = audit(&parse_quote! {
struct Money {
#[serde(with = "string_money")]
amount: i64,
}
})
.expect_err("a `with` adapter must be refused");
assert!(err.contains("with"), "{err}");
}
#[test]
fn a_field_deserialize_adapter_is_refused_too() {
let err = audit(&parse_quote! {
struct Money {
#[serde(deserialize_with = "lenient")]
amount: i64,
}
})
.expect_err("a deserialize adapter must be refused");
assert!(err.contains("deserialize_with"), "{err}");
}
#[test]
fn a_plain_struct_still_passes() {
audit(&parse_quote! {
struct Plain {
id: i64,
#[serde(rename = "displayName")]
name: String,
tags: Option<Vec<String>>,
}
})
.expect("a plain struct must still be describable");
}
#[test]
fn skip_serializing_if_alone_is_still_refused() {
let err = audit(&parse_quote! {
struct Row {
#[serde(skip_serializing_if = "String::is_empty")]
tags: String,
}
})
.expect_err("a bare skip_serializing_if on a non-Option field must be refused");
assert!(err.contains("skip_serializing_if"), "{err}");
}
#[test]
fn a_field_default_rescues_skip_serializing_if() {
audit(&parse_quote! {
struct Row {
#[serde(default, skip_serializing_if = "String::is_empty")]
tags: String,
}
})
.expect("default + skip_serializing_if is describable");
}
#[test]
fn a_valued_field_default_rescues_skip_serializing_if() {
audit(&parse_quote! {
struct Row {
#[serde(default = "empty_tags", skip_serializing_if = "String::is_empty")]
tags: String,
}
})
.expect("default = \"path\" + skip_serializing_if is describable");
}
#[test]
fn a_container_default_rescues_skip_serializing_if() {
audit(&parse_quote! {
#[serde(default)]
struct Row {
#[serde(skip_serializing_if = "String::is_empty")]
tags: String,
}
})
.expect("a container default makes every field omissible on input");
}
#[test]
fn a_field_alias_is_refused() {
let err = audit(&parse_quote! {
struct Row {
#[serde(alias = "legacy_name")]
name: String,
}
})
.expect_err("an aliased field must be refused");
assert!(err.contains("alias"), "{err}");
}
#[test]
fn a_field_alias_after_a_list_valued_attribute_is_still_seen() {
let err = audit(&parse_quote! {
struct Row {
#[serde(bound(deserialize = "T: Clone"), alias = "legacy_name")]
name: String,
}
})
.expect_err("a field alias behind a list-valued sibling must still be found");
assert!(err.contains("alias"), "{err}");
}
#[test]
fn a_renamed_field_is_still_describable() {
audit(&parse_quote! {
struct Row {
#[serde(rename = "displayName")]
name: String,
}
})
.expect("a symmetric field rename must stay describable");
}
fn audit_enum(input: &DeriveInput) -> Result<(), String> {
let Data::Enum(data) = &input.data else {
panic!("fixture must be an enum");
};
reject_undescribable_enum(input, data).map_err(|e| e.to_string())
}
#[test]
fn a_variant_alias_is_refused() {
let err = audit_enum(&parse_quote! {
enum Status {
Active,
#[serde(alias = "legacy")]
Retired,
}
})
.expect_err("an aliased variant must be refused");
assert!(err.contains("alias"), "{err}");
}
#[test]
fn an_alias_after_a_list_valued_attribute_is_still_seen() {
let err = audit_enum(&parse_quote! {
#[serde(bound(deserialize = "T: Clone"))]
enum Status {
Active,
#[serde(bound(deserialize = "T: Clone"), alias = "legacy")]
Retired,
}
})
.expect_err("an alias behind a list-valued sibling must still be found");
assert!(err.contains("alias"), "{err}");
}
#[test]
fn a_variant_level_untagged_is_refused() {
let err = audit_enum(&parse_quote! {
enum Status {
Active,
Retired,
#[serde(untagged)]
Unknown,
}
})
.expect_err("a variant-level untagged must be refused");
assert!(err.contains("untagged"), "{err}");
}
#[test]
fn a_catch_all_variant_is_refused() {
let err = audit_enum(&parse_quote! {
enum Status {
Active,
Retired,
#[serde(other)]
Unknown,
}
})
.expect_err("a #[serde(other)] catch-all must be refused");
assert!(err.contains("other"), "{err}");
}
#[test]
fn a_container_level_untagged_is_still_refused() {
let err = audit_enum(&parse_quote! {
#[serde(untagged)]
enum Status {
Active,
Retired,
}
})
.expect_err("a container-level untagged must still be refused");
assert!(err.contains("untagged"), "{err}");
}
#[test]
fn a_plain_unit_enum_is_still_describable() {
audit_enum(&parse_quote! {
enum Status {
Active,
Retired,
}
})
.expect("a plain unit enum must stay describable");
}
#[test]
fn a_renamed_variant_is_still_describable() {
audit_enum(&parse_quote! {
enum Status {
Active,
#[serde(rename = "retired")]
Retired,
}
})
.expect("a symmetric rename must still be describable");
}
#[test]
fn a_lookalike_key_is_not_an_adapter() {
audit(&parse_quote! {
struct Plain {
#[serde(default = "with_default")]
amount: i64,
}
})
.expect("`default = \"with_default\"` is not an adapter");
}
}