use crate::types::{EmitError, RenameAll, TypePath};
#[derive(Debug, Clone, Default)]
pub(crate) struct ContainerAttrs {
pub rename_all: Option<RenameAll>,
pub rename: Option<String>,
pub rename_all_fields: Option<RenameAll>,
pub default: bool,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct FieldAttrs {
pub rename: Option<String>,
pub skip: bool,
pub default: bool,
pub flatten: bool,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct VariantAttrs {
pub rename: Option<String>,
pub skip: bool,
pub rename_all: Option<RenameAll>,
}
impl RenameAll {
pub(crate) fn from_serde_str(s: &str) -> Option<Self> {
Some(match s {
"lowercase" => Self::Lowercase,
"UPPERCASE" => Self::Uppercase,
"PascalCase" => Self::PascalCase,
"camelCase" => Self::CamelCase,
"snake_case" => Self::SnakeCase,
"SCREAMING_SNAKE_CASE" => Self::ScreamingSnakeCase,
"kebab-case" => Self::KebabCase,
"SCREAMING-KEBAB-CASE" => Self::ScreamingKebabCase,
_ => return None,
})
}
}
const REJECTED_SHAPE_ATTRS: &[&str] = &["tag", "content", "untagged"];
const REJECTED_SHAPE_HINT: &str = "shape-changing attrs (tag/content/untagged) are phase 2 work; use \
#[ontogen::ts_opaque(target = \"...\")] if a custom TS rendering is needed";
#[derive(Debug, Clone, Default)]
pub(crate) struct OntogenAttrs {
pub ts_opaque: Option<String>,
pub ts_name: Option<String>,
}
pub(crate) fn extract_ontogen_attrs(
attrs: &[syn::Attribute],
referenced_by: &TypePath,
) -> Result<OntogenAttrs, EmitError> {
let mut out = OntogenAttrs::default();
for attr in attrs {
let terminal = match attr.path().segments.last() {
Some(seg) => seg.ident.to_string(),
None => continue,
};
match terminal.as_str() {
"ts_opaque" => {
let mut target: Option<String> = None;
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("target") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
target = Some(lit.value());
Ok(())
} else {
Err(meta.error("ts_opaque expects `target = \"...\"`"))
}
})
.map_err(|err| EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: format!("could not parse #[ts_opaque(...)]: {err}"),
})?;
out.ts_opaque = target;
}
"ts_name" => {
let value = match &attr.meta {
syn::Meta::NameValue(nv) => &nv.value,
_ => {
return Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: "#[ts_name = \"...\"] expects the `= \"literal\"` form".to_string(),
});
}
};
let lit = match value {
syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Str(s) => s.value(),
_ => {
return Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: "#[ts_name = ...] value must be a string literal".to_string(),
});
}
},
_ => {
return Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: "#[ts_name = ...] value must be a string literal".to_string(),
});
}
};
out.ts_name = Some(lit);
}
_ => {}
}
}
Ok(out)
}
fn parse_rename_all(value: &str, attr_name: &str, referenced_by: &TypePath) -> Result<RenameAll, EmitError> {
RenameAll::from_serde_str(value).ok_or_else(|| EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: format!("{attr_name} = \"{value}\" (not one of serde's eight recognized modes)"),
})
}
pub(crate) fn extract_container_attrs(
attrs: &[syn::Attribute],
referenced_by: &TypePath,
) -> Result<ContainerAttrs, EmitError> {
let mut out = ContainerAttrs::default();
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
walk_serde(attr, |meta_kind| {
match meta_kind {
MetaKind::RenameLit(value) => {
out.rename = Some(value);
Ok(())
}
MetaKind::RenameAllLit(value) => {
out.rename_all = Some(parse_rename_all(&value, "rename_all", referenced_by)?);
Ok(())
}
MetaKind::RenameAllFieldsLit(value) => {
out.rename_all_fields = Some(parse_rename_all(&value, "rename_all_fields", referenced_by)?);
Ok(())
}
MetaKind::SplitRename | MetaKind::SplitRenameAll => Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: "split-rename (rename(serialize = \"...\", deserialize = \"...\")) is not supported in \
phase 1 — use the symmetric form #[serde(rename = \"...\")] or \
#[ontogen::ts_opaque(target = \"...\")] if the serde asymmetry must be preserved for \
non-ontogen-ts consumers"
.to_string(),
}),
MetaKind::RejectedShape(name) => Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: format!("serde({name}) — {REJECTED_SHAPE_HINT}"),
}),
MetaKind::Flatten => Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: "serde(flatten) is a field attribute — serde does not accept it on a struct or enum \
declaration"
.to_string(),
}),
MetaKind::Skip => Ok(()), MetaKind::Default => {
out.default = true;
Ok(())
}
MetaKind::Unknown => Ok(()),
}
})?;
}
Ok(out)
}
pub(crate) fn extract_field_attrs(attrs: &[syn::Attribute], referenced_by: &TypePath) -> Result<FieldAttrs, EmitError> {
let mut out = FieldAttrs::default();
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
walk_serde(attr, |meta_kind| {
match meta_kind {
MetaKind::RenameLit(value) => {
out.rename = Some(value);
Ok(())
}
MetaKind::RenameAllLit(_) | MetaKind::RenameAllFieldsLit(_) => Ok(()),
MetaKind::Skip => {
out.skip = true;
Ok(())
}
MetaKind::Default => {
out.default = true;
Ok(())
}
MetaKind::Flatten => {
out.flatten = true;
Ok(())
}
MetaKind::SplitRename => Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: "split-rename (rename(serialize = \"...\", deserialize = \"...\")) on a field is not \
supported in phase 1 — use the symmetric form #[serde(rename = \"...\")] or \
#[ontogen::ts_opaque(target = \"...\")] on the parent type"
.to_string(),
}),
MetaKind::RejectedShape(name) => Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: format!("serde({name}) on a field — {REJECTED_SHAPE_HINT}"),
}),
MetaKind::SplitRenameAll | MetaKind::Unknown => Ok(()),
}
})?;
}
Ok(out)
}
pub(crate) fn extract_variant_attrs(
attrs: &[syn::Attribute],
referenced_by: &TypePath,
) -> Result<VariantAttrs, EmitError> {
let mut out = VariantAttrs::default();
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
walk_serde(attr, |meta_kind| match meta_kind {
MetaKind::RenameLit(value) => {
out.rename = Some(value);
Ok(())
}
MetaKind::Skip => {
out.skip = true;
Ok(())
}
MetaKind::SplitRename => Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: "split-rename on a variant is not supported in phase 1 — use the symmetric form \
#[serde(rename = \"...\")]"
.to_string(),
}),
MetaKind::RejectedShape(name) => Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: format!("serde({name}) on a variant — {REJECTED_SHAPE_HINT}"),
}),
MetaKind::Flatten => Err(EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: "serde(flatten) is a field attribute — serde does not accept it on an enum variant".to_string(),
}),
MetaKind::RenameAllLit(value) => {
out.rename_all = Some(parse_rename_all(&value, "rename_all", referenced_by)?);
Ok(())
}
MetaKind::RenameAllFieldsLit(_) | MetaKind::SplitRenameAll | MetaKind::Default | MetaKind::Unknown => {
Ok(())
}
})?;
}
Ok(out)
}
enum MetaKind {
RenameLit(String),
RenameAllLit(String),
RenameAllFieldsLit(String),
SplitRename,
SplitRenameAll,
RejectedShape(String),
Flatten,
Skip,
Default,
Unknown,
}
fn consume_inner_value(inner: syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
if let Ok(value) = inner.value() {
let _: syn::Lit = value.parse()?;
}
Ok(())
}
fn walk_serde<F>(attr: &syn::Attribute, mut f: F) -> Result<(), EmitError>
where
F: FnMut(MetaKind) -> Result<(), EmitError>,
{
let mut callbacks: Vec<MetaKind> = Vec::new();
let parse_result = attr.parse_nested_meta(|meta| {
let ident = match meta.path.get_ident() {
Some(id) => id.to_string(),
None => {
callbacks.push(MetaKind::Unknown);
return Ok(());
}
};
match ident.as_str() {
"rename" => {
match meta.value() {
Ok(value) => {
let lit: syn::LitStr = value.parse().map_err(|_| meta.error("expected string literal"))?;
callbacks.push(MetaKind::RenameLit(lit.value()));
}
Err(_) => {
meta.parse_nested_meta(consume_inner_value)?;
callbacks.push(MetaKind::SplitRename);
}
}
Ok(())
}
"rename_all" | "rename_all_fields" => {
let is_fields = ident == "rename_all_fields";
match meta.value() {
Ok(value) => {
let lit: syn::LitStr = value.parse().map_err(|_| meta.error("expected string literal"))?;
callbacks.push(if is_fields {
MetaKind::RenameAllFieldsLit(lit.value())
} else {
MetaKind::RenameAllLit(lit.value())
});
}
Err(_) => {
meta.parse_nested_meta(consume_inner_value)?;
callbacks.push(MetaKind::SplitRenameAll);
}
}
Ok(())
}
"skip" | "skip_serializing" | "skip_deserializing" => {
callbacks.push(MetaKind::Skip);
Ok(())
}
"default" => {
if let Ok(value) = meta.value() {
let _: syn::LitStr = value.parse().map_err(|_| meta.error("expected string literal"))?;
}
callbacks.push(MetaKind::Default);
Ok(())
}
"flatten" => {
if let Ok(value) = meta.value() {
let _: syn::Lit = value.parse().map_err(|_| meta.error("expected literal"))?;
}
callbacks.push(MetaKind::Flatten);
Ok(())
}
other if REJECTED_SHAPE_ATTRS.contains(&other) => {
if let Ok(value) = meta.value() {
let _: syn::LitStr = value.parse().map_err(|_| meta.error("expected string literal"))?;
}
callbacks.push(MetaKind::RejectedShape(other.to_string()));
Ok(())
}
_ => {
if let Ok(value) = meta.value() {
let _: syn::Lit = value.parse().map_err(|_| meta.error("expected literal"))?;
} else {
let _ = meta.parse_nested_meta(consume_inner_value);
}
callbacks.push(MetaKind::Unknown);
Ok(())
}
}
});
if let Err(err) = parse_result {
return Err(EmitError::UnsupportedSerdeAttr {
type_path: TypePath::new(vec!["<unknown>".to_string()]).expect("non-empty"),
attr: format!("could not parse #[serde(...)]: {err}"),
});
}
for cb in callbacks {
f(cb)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{EmitError, TypePath};
fn tp(name: &str) -> TypePath {
TypePath::new(vec![name.to_string()]).expect("non-empty")
}
fn struct_attrs(src: &str) -> Vec<syn::Attribute> {
let item: syn::ItemStruct = syn::parse_str(src).expect("parse struct");
item.attrs
}
fn enum_attrs(src: &str) -> Vec<syn::Attribute> {
let item: syn::ItemEnum = syn::parse_str(src).expect("parse enum");
item.attrs
}
fn first_field_attrs(src: &str) -> Vec<syn::Attribute> {
let item: syn::ItemStruct = syn::parse_str(src).expect("parse struct");
let syn::Fields::Named(named) = item.fields else {
panic!("test fixture must use named fields");
};
named.named.into_iter().next().expect("at least one field").attrs
}
fn first_variant_attrs(src: &str) -> Vec<syn::Attribute> {
let item: syn::ItemEnum = syn::parse_str(src).expect("parse enum");
item.variants.into_iter().next().expect("at least one variant").attrs
}
#[test]
fn container_rename_all_camel_case() {
let attrs = struct_attrs(
r#"
#[serde(rename_all = "camelCase")]
struct Foo { a: u32 }
"#,
);
let out = extract_container_attrs(&attrs, &tp("Foo")).unwrap();
assert_eq!(out.rename_all, Some(RenameAll::CamelCase));
}
#[test]
fn container_rename_all_all_eight_modes() {
let pairs = [
("lowercase", RenameAll::Lowercase),
("UPPERCASE", RenameAll::Uppercase),
("PascalCase", RenameAll::PascalCase),
("camelCase", RenameAll::CamelCase),
("snake_case", RenameAll::SnakeCase),
("SCREAMING_SNAKE_CASE", RenameAll::ScreamingSnakeCase),
("kebab-case", RenameAll::KebabCase),
("SCREAMING-KEBAB-CASE", RenameAll::ScreamingKebabCase),
];
for (src, expected) in pairs {
let attrs = struct_attrs(&format!(r#"#[serde(rename_all = "{src}")] struct Foo {{ a: u32 }}"#));
let out = extract_container_attrs(&attrs, &tp("Foo")).unwrap();
assert_eq!(out.rename_all, Some(expected), "rename_all = \"{src}\"");
}
}
#[test]
fn container_rename_all_unknown_mode_rejected() {
let attrs = struct_attrs(
r#"
#[serde(rename_all = "Train-Case")]
struct Foo { a: u32 }
"#,
);
let err = extract_container_attrs(&attrs, &tp("Foo")).unwrap_err();
match err {
EmitError::UnsupportedSerdeAttr { attr, .. } => {
assert!(attr.contains("Train-Case"), "attr was: {attr}");
assert!(attr.contains("recognized modes"), "attr was: {attr}");
}
other => panic!("expected UnsupportedSerdeAttr, got {other:?}"),
}
}
#[test]
fn container_split_rename_all_rejected() {
let attrs = struct_attrs(
r#"
#[serde(rename_all(serialize = "camelCase", deserialize = "snake_case"))]
struct Foo { a: u32 }
"#,
);
let err = extract_container_attrs(&attrs, &tp("Foo")).unwrap_err();
match err {
EmitError::UnsupportedSerdeAttr { attr, .. } => {
assert!(attr.contains("split-rename"), "attr was: {attr}");
}
other => panic!("expected UnsupportedSerdeAttr, got {other:?}"),
}
}
#[test]
fn container_tag_rejected() {
let attrs = enum_attrs(
r#"
#[serde(tag = "type")]
enum Msg { Click, Hover }
"#,
);
let err = extract_container_attrs(&attrs, &tp("Msg")).unwrap_err();
match err {
EmitError::UnsupportedSerdeAttr { attr, .. } => {
assert!(attr.contains("tag"), "attr was: {attr}");
assert!(attr.contains("phase 2"), "attr was: {attr}");
}
other => panic!("expected UnsupportedSerdeAttr, got {other:?}"),
}
}
#[test]
fn container_untagged_rejected() {
let attrs = enum_attrs(
r#"
#[serde(untagged)]
enum U { A(u32), B(String) }
"#,
);
let err = extract_container_attrs(&attrs, &tp("U")).unwrap_err();
assert!(matches!(err, EmitError::UnsupportedSerdeAttr { .. }));
}
#[test]
fn field_rename() {
let attrs = first_field_attrs(
r#"
struct Foo {
#[serde(rename = "wireName")]
pub a: u32,
}
"#,
);
let out = extract_field_attrs(&attrs, &tp("Foo")).unwrap();
assert_eq!(out.rename.as_deref(), Some("wireName"));
assert!(!out.skip);
}
#[test]
fn field_skip() {
let attrs = first_field_attrs(
r#"
struct Foo {
#[serde(skip)]
pub a: u32,
}
"#,
);
let out = extract_field_attrs(&attrs, &tp("Foo")).unwrap();
assert!(out.skip);
}
#[test]
fn field_skip_serializing_treated_as_skip() {
let attrs = first_field_attrs(
r#"
struct Foo {
#[serde(skip_serializing)]
pub a: u32,
}
"#,
);
let out = extract_field_attrs(&attrs, &tp("Foo")).unwrap();
assert!(out.skip);
}
#[test]
fn field_split_rename_rejected() {
let attrs = first_field_attrs(
r#"
struct Foo {
#[serde(rename(serialize = "wire_name", deserialize = "wireName"))]
pub a: u32,
}
"#,
);
let err = extract_field_attrs(&attrs, &tp("Foo")).unwrap_err();
match err {
EmitError::UnsupportedSerdeAttr { attr, .. } => {
assert!(attr.contains("split-rename"), "attr was: {attr}");
assert!(attr.contains("on a field"), "attr was: {attr}");
}
other => panic!("expected UnsupportedSerdeAttr, got {other:?}"),
}
}
#[test]
fn field_no_serde_attrs_returns_default() {
let attrs = first_field_attrs(
r#"
struct Foo {
pub a: u32,
}
"#,
);
let out = extract_field_attrs(&attrs, &tp("Foo")).unwrap();
assert!(out.rename.is_none());
assert!(!out.skip);
}
#[test]
fn field_default_bare_sets_flag() {
let attrs = first_field_attrs(
r#"
struct Foo {
#[serde(default)]
pub a: u32,
}
"#,
);
let out = extract_field_attrs(&attrs, &tp("Foo")).unwrap();
assert!(out.default, "bare #[serde(default)] should set the default flag");
assert!(out.rename.is_none());
assert!(!out.skip);
}
#[test]
fn field_default_path_form_sets_flag() {
let attrs = first_field_attrs(
r#"
struct Foo {
#[serde(default = "defaults::a")]
pub a: u32,
}
"#,
);
let out = extract_field_attrs(&attrs, &tp("Foo")).unwrap();
assert!(out.default, "path-form #[serde(default = \"...\")] should set the default flag");
}
#[test]
fn field_without_default_leaves_flag_unset() {
let attrs = first_field_attrs(
r#"
struct Foo {
pub a: u32,
}
"#,
);
let out = extract_field_attrs(&attrs, &tp("Foo")).unwrap();
assert!(!out.default);
}
#[test]
fn container_default_sets_flag() {
let attrs = struct_attrs(
r#"
#[serde(default)]
struct Foo { a: u32 }
"#,
);
assert!(extract_container_attrs(&attrs, &tp("Foo")).unwrap().default);
}
#[test]
fn container_default_path_form_sets_flag() {
let attrs = struct_attrs(
r#"
#[serde(default = "defaults::foo")]
struct Foo { a: u32 }
"#,
);
assert!(extract_container_attrs(&attrs, &tp("Foo")).unwrap().default);
}
#[test]
fn container_without_default_leaves_flag_unset() {
let attrs = struct_attrs(
r#"
#[serde(rename_all = "camelCase")]
struct Foo { a: u32 }
"#,
);
assert!(!extract_container_attrs(&attrs, &tp("Foo")).unwrap().default);
}
#[test]
fn container_rename_all_fields_is_its_own_axis() {
let attrs = enum_attrs(
r#"
#[serde(rename_all = "camelCase", rename_all_fields = "SCREAMING_SNAKE_CASE")]
enum Event { ToolCall { prompt_template: String } }
"#,
);
let out = extract_container_attrs(&attrs, &tp("Event")).unwrap();
assert_eq!(out.rename_all, Some(RenameAll::CamelCase));
assert_eq!(out.rename_all_fields, Some(RenameAll::ScreamingSnakeCase));
}
#[test]
fn container_rename_all_leaves_rename_all_fields_unset() {
let attrs = enum_attrs(
r#"
#[serde(rename_all = "camelCase")]
enum Event { ToolCall { prompt_template: String } }
"#,
);
let out = extract_container_attrs(&attrs, &tp("Event")).unwrap();
assert_eq!(out.rename_all, Some(RenameAll::CamelCase));
assert_eq!(out.rename_all_fields, None, "rename_all must not imply rename_all_fields");
}
#[test]
fn container_rename_all_fields_unknown_mode_names_the_right_attr() {
let attrs = enum_attrs(
r#"
#[serde(rename_all_fields = "Train-Case")]
enum Event { ToolCall { prompt_template: String } }
"#,
);
match extract_container_attrs(&attrs, &tp("Event")).unwrap_err() {
EmitError::UnsupportedSerdeAttr { attr, .. } => {
assert!(attr.contains("rename_all_fields"), "attr was: {attr}");
assert!(attr.contains("Train-Case"), "attr was: {attr}");
}
other => panic!("expected UnsupportedSerdeAttr, got {other:?}"),
}
}
#[test]
fn variant_rename_all_is_captured() {
let attrs = first_variant_attrs(
r#"
enum Event {
#[serde(rename_all = "camelCase")]
ToolCall { prompt_template: String },
}
"#,
);
let out = extract_variant_attrs(&attrs, &tp("Event")).unwrap();
assert_eq!(out.rename_all, Some(RenameAll::CamelCase));
assert!(out.rename.is_none(), "rename_all must not set the variant's own wire name");
}
#[test]
fn variant_rename_and_rename_all_are_independent() {
let attrs = first_variant_attrs(
r#"
enum Event {
#[serde(rename = "call", rename_all = "UPPERCASE")]
ToolCall { prompt_template: String },
}
"#,
);
let out = extract_variant_attrs(&attrs, &tp("Event")).unwrap();
assert_eq!(out.rename.as_deref(), Some("call"));
assert_eq!(out.rename_all, Some(RenameAll::Uppercase));
}
#[test]
fn field_rename_all_family_is_ignored() {
for src in ["rename_all = \"camelCase\"", "rename_all_fields = \"camelCase\""] {
let attrs = first_field_attrs(&format!(
r#"
struct Foo {{
#[serde({src})]
pub prompt_template: String,
}}
"#
));
let out = extract_field_attrs(&attrs, &tp("Foo")).unwrap();
assert!(out.rename.is_none(), "src `{src}` should be inert on a field");
assert!(!out.skip);
}
}
#[test]
fn field_flatten_sets_flag() {
let attrs = first_field_attrs(
r#"
struct Step {
#[serde(flatten)]
pub meta: StepMeta,
}
"#,
);
let out = extract_field_attrs(&attrs, &tp("Step")).unwrap();
assert!(out.flatten, "#[serde(flatten)] should set the flatten flag");
assert!(out.rename.is_none());
assert!(!out.skip);
assert!(!out.default);
}
#[test]
fn field_flatten_composes_with_default() {
let attrs = first_field_attrs(
r#"
struct Step {
#[serde(flatten, default)]
pub meta: StepMeta,
}
"#,
);
let out = extract_field_attrs(&attrs, &tp("Step")).unwrap();
assert!(out.flatten);
assert!(out.default);
}
#[test]
fn field_without_flatten_leaves_flag_unset() {
let attrs = first_field_attrs(
r#"
struct Step {
pub meta: StepMeta,
}
"#,
);
assert!(!extract_field_attrs(&attrs, &tp("Step")).unwrap().flatten);
}
#[test]
fn container_flatten_rejected() {
let attrs = struct_attrs(
r#"
#[serde(flatten)]
struct Foo { a: u32 }
"#,
);
let err = extract_container_attrs(&attrs, &tp("Foo")).unwrap_err();
match err {
EmitError::UnsupportedSerdeAttr { attr, .. } => {
assert!(attr.contains("field attribute"), "attr was: {attr}");
}
other => panic!("expected UnsupportedSerdeAttr, got {other:?}"),
}
}
#[test]
fn variant_flatten_rejected() {
let attrs = first_variant_attrs(
r#"
enum E {
#[serde(flatten)]
A,
}
"#,
);
let err = extract_variant_attrs(&attrs, &tp("E")).unwrap_err();
assert!(matches!(err, EmitError::UnsupportedSerdeAttr { .. }));
}
#[test]
fn field_level_container_shape_attrs_rejected() {
for src in ["tag = \"type\"", "content = \"c\"", "untagged"] {
let attrs = first_field_attrs(&format!(
r#"
struct Foo {{
#[serde({src})]
pub a: u32,
}}
"#
));
let err = extract_field_attrs(&attrs, &tp("Foo")).unwrap_err();
match err {
EmitError::UnsupportedSerdeAttr { attr, .. } => {
assert!(attr.contains("on a field"), "src `{src}` — attr was: {attr}");
}
other => panic!("expected UnsupportedSerdeAttr for `{src}`, got {other:?}"),
}
}
}
#[test]
fn variant_level_untagged_rejected() {
let attrs = first_variant_attrs(
r#"
enum U {
#[serde(untagged)]
Other(String),
}
"#,
);
let err = extract_variant_attrs(&attrs, &tp("U")).unwrap_err();
match err {
EmitError::UnsupportedSerdeAttr { attr, .. } => {
assert!(attr.contains("on a variant"), "attr was: {attr}");
}
other => panic!("expected UnsupportedSerdeAttr, got {other:?}"),
}
}
#[test]
fn variant_rename() {
let attrs = first_variant_attrs(
r#"
enum Color {
#[serde(rename = "rouge")]
Red,
}
"#,
);
let out = extract_variant_attrs(&attrs, &tp("Color")).unwrap();
assert_eq!(out.rename.as_deref(), Some("rouge"));
}
#[test]
fn variant_split_rename_rejected() {
let attrs = first_variant_attrs(
r#"
enum Color {
#[serde(rename(serialize = "Red", deserialize = "red"))]
Red,
}
"#,
);
let err = extract_variant_attrs(&attrs, &tp("Color")).unwrap_err();
match err {
EmitError::UnsupportedSerdeAttr { attr, .. } => {
assert!(attr.contains("split-rename"), "attr was: {attr}");
assert!(attr.contains("variant"), "attr was: {attr}");
}
other => panic!("expected UnsupportedSerdeAttr, got {other:?}"),
}
}
#[test]
fn container_rename_and_rename_all_both_parsed() {
let attrs = struct_attrs(
r#"
#[serde(rename = "FooDto", rename_all = "camelCase")]
struct Foo { a: u32 }
"#,
);
let out = extract_container_attrs(&attrs, &tp("Foo")).unwrap();
assert_eq!(out.rename.as_deref(), Some("FooDto"));
assert_eq!(out.rename_all, Some(RenameAll::CamelCase));
}
}