Skip to main content

metaxy_cli/parser/
serde.rs

1use crate::model::{EnumTagging, RenameRule};
2
3/// Walks `#[serde(...)]` attributes and calls `visitor` for each nested meta item.
4/// Returns the last value produced by the visitor, or `None` if no match.
5fn find_serde_meta<T>(
6    attrs: &[syn::Attribute],
7    mut visitor: impl FnMut(&syn::meta::ParseNestedMeta) -> Option<T>,
8) -> Option<T> {
9    let mut result = None;
10    for attr in attrs {
11        if !attr.path().is_ident("serde") {
12            continue;
13        }
14        let _ = attr.parse_nested_meta(|meta| {
15            if let Some(value) = visitor(&meta) {
16                result = Some(value);
17            }
18            Ok(())
19        });
20    }
21    result
22}
23
24/// Parses `#[serde(rename_all = "...")]` from attributes.
25pub fn parse_rename_all(attrs: &[syn::Attribute]) -> Option<RenameRule> {
26    find_serde_meta(attrs, |meta| {
27        if !meta.path.is_ident("rename_all") {
28            return None;
29        }
30        let value = meta.value().ok()?.parse::<syn::LitStr>().ok()?;
31        match value.value().parse::<RenameRule>() {
32            Ok(rule) => Some(rule),
33            Err(e) => {
34                eprintln!(
35                    "warning: unknown rename_all value \"{}\" — {e}; attribute ignored",
36                    value.value(),
37                );
38                None
39            }
40        }
41    })
42}
43
44/// Parses `#[serde(rename = "...")]` from attributes.
45pub fn parse_rename(attrs: &[syn::Attribute]) -> Option<String> {
46    find_serde_meta(attrs, |meta| {
47        if !meta.path.is_ident("rename") {
48            return None;
49        }
50        let value = meta.value().ok()?.parse::<syn::LitStr>().ok()?;
51        Some(value.value())
52    })
53}
54
55/// Checks for `#[serde(flatten)]` on a field.
56pub fn is_flattened(attrs: &[syn::Attribute]) -> bool {
57    find_serde_meta(attrs, |meta| {
58        if meta.path.is_ident("flatten") {
59            Some(true)
60        } else {
61            None
62        }
63    })
64    .unwrap_or(false)
65}
66
67/// Checks for `#[serde(skip)]` or `#[serde(skip_serializing)]` on a field.
68pub fn is_skipped(attrs: &[syn::Attribute]) -> bool {
69    find_serde_meta(attrs, |meta| {
70        if meta.path.is_ident("skip") || meta.path.is_ident("skip_serializing") {
71            Some(true)
72        } else {
73            None
74        }
75    })
76    .unwrap_or(false)
77}
78
79/// Parses the serde enum tagging strategy from attributes.
80///
81/// Recognizes `#[serde(tag = "...", content = "...")]` and `#[serde(untagged)]`.
82/// Priority: `untagged` → `Untagged`; `tag` + `content` → `Adjacent`;
83/// `tag` only → `Internal`; else → `External`.
84pub fn parse_enum_tagging(attrs: &[syn::Attribute]) -> EnumTagging {
85    let mut tag = None;
86    let mut content = None;
87    let mut untagged = false;
88
89    for attr in attrs {
90        if !attr.path().is_ident("serde") {
91            continue;
92        }
93        let _ = attr.parse_nested_meta(|meta| {
94            if meta.path.is_ident("untagged") {
95                untagged = true;
96            } else if meta.path.is_ident("tag")
97                && let Ok(value) = meta.value()
98                && let Ok(lit) = value.parse::<syn::LitStr>()
99            {
100                tag = Some(lit.value());
101            } else if meta.path.is_ident("content")
102                && let Ok(value) = meta.value()
103                && let Ok(lit) = value.parse::<syn::LitStr>()
104            {
105                content = Some(lit.value());
106            }
107            Ok(())
108        });
109    }
110
111    if untagged {
112        EnumTagging::Untagged
113    } else if let Some(tag) = tag {
114        if let Some(content) = content {
115            EnumTagging::Adjacent { tag, content }
116        } else {
117            EnumTagging::Internal { tag }
118        }
119    } else {
120        EnumTagging::External
121    }
122}
123
124/// Checks for `#[serde(default)]` on a field.
125pub fn has_default(attrs: &[syn::Attribute]) -> bool {
126    find_serde_meta(attrs, |meta| {
127        if meta.path.is_ident("default") {
128            Some(true)
129        } else {
130            None
131        }
132    })
133    .unwrap_or(false)
134}