use crate::types::{EmitError, RenameAll, TypePath};
#[derive(Debug, Clone, Default)]
pub(crate) struct ContainerAttrs {
pub rename_all: Option<RenameAll>,
pub rename: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct FieldAttrs {
pub rename: Option<String>,
pub skip: bool,
pub default: bool,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct VariantAttrs {
pub rename: Option<String>,
pub skip: bool,
}
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", "flatten"];
#[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)
}
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) => {
let mode = RenameAll::from_serde_str(&value).ok_or_else(|| EmitError::UnsupportedSerdeAttr {
type_path: referenced_by.clone(),
attr: format!("rename_all = \"{value}\" (not one of serde's eight recognized modes)"),
})?;
out.rename_all = Some(mode);
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}) — shape-changing attrs (tag/content/untagged/flatten) are phase 2 work; \
use #[ontogen::ts_opaque(target = \"...\")] if a custom TS rendering is needed"
),
}),
MetaKind::Skip => Ok(()), MetaKind::Default => 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(_) => Ok(()), MetaKind::Skip => {
out.skip = true;
Ok(())
}
MetaKind::Default => {
out.default = 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::SplitRenameAll | MetaKind::RejectedShape(_) | 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::RenameAllLit(_)
| MetaKind::SplitRenameAll
| MetaKind::RejectedShape(_)
| MetaKind::Default
| MetaKind::Unknown => Ok(()),
})?;
}
Ok(out)
}
enum MetaKind {
RenameLit(String),
RenameAllLit(String),
SplitRename,
SplitRenameAll,
RejectedShape(String),
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" => {
match meta.value() {
Ok(value) => {
let lit: syn::LitStr = value.parse().map_err(|_| meta.error("expected string literal"))?;
callbacks.push(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(())
}
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_is_ignored() {
let attrs = struct_attrs(
r#"
#[serde(default)]
struct Foo { a: u32 }
"#,
);
extract_container_attrs(&attrs, &tp("Foo")).unwrap();
}
#[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));
}
}