Skip to main content

wowlab_engine_macros_impl/
copy_insert_parse.rs

1use syn::{DeriveInput, Field, Ident, LitStr, Path};
2
3const DUPLICATE_BIND_MODE: &str = "duplicate `#[copy(...)]` bind mode on field";
4
5pub(crate) struct CopyInsertContainer {
6    pub(crate) patch_first: bool,
7    pub(crate) support: Path,
8}
9
10pub(crate) enum CopyBindMode {
11    Default,
12    Json,
13    EnumStr,
14    With(Path),
15}
16
17pub(crate) struct CopyFieldPlan {
18    pub(crate) ident: Ident,
19    pub(crate) skip: bool,
20    pub(crate) mode: CopyBindMode,
21    pub(crate) rename: Option<LitStr>,
22}
23
24pub(crate) fn parse_copy_insert_container(input: &DeriveInput) -> syn::Result<CopyInsertContainer> {
25    let mut patch_first = false;
26    let mut support: Option<Path> = None;
27    let mut container_attr: Option<&syn::Attribute> = None;
28
29    for attr in &input.attrs {
30        if !attr.path().is_ident("copy_insert") {
31            continue;
32        }
33
34        if container_attr.is_some() {
35            return Err(syn::Error::new_spanned(
36                attr,
37                "duplicate #[copy_insert(...)] attribute",
38            ));
39        }
40
41        container_attr = Some(attr);
42        let mut saw_patch_first = false;
43
44        attr.parse_nested_meta(|meta| {
45            attr_keys!(
46                meta,
47                unknown = "unknown `copy_insert` key, expected `patch_first` or `crate = \"...\"`";
48                "patch_first" => (
49                    saw_patch_first,
50                    "duplicate `patch_first` copy_insert key"
51                ) {
52                saw_patch_first = true;
53                patch_first = true;
54                },
55                "crate" => (support.is_some(), "duplicate `crate` copy_insert key") {
56                let lit: LitStr = meta.value()?.parse()?;
57
58                support = Some(lit.parse().map_err(|error| {
59                    syn::Error::new_spanned(
60                        &lit,
61                        format!(
62                            "#[copy_insert(crate = \"...\")] expects a valid Rust path: {error}"
63                        ),
64                    )
65                })?);
66                }
67            )
68        })?;
69    }
70
71    Ok(CopyInsertContainer {
72        patch_first,
73        support: support.unwrap_or_else(|| syn::parse_quote!(crate::copy)),
74    })
75}
76
77pub(crate) fn parse_copy_field(field: &Field) -> syn::Result<CopyFieldPlan> {
78    let ident = field
79        .ident
80        .clone()
81        .ok_or_else(|| syn::Error::new_spanned(field, "field must be named"))?;
82    let mut mode: Option<CopyBindMode> = None;
83    let mut rename: Option<LitStr> = None;
84    let mut skip = false;
85
86    for attr in &field.attrs {
87        if !attr.path().is_ident("copy") {
88            continue;
89        }
90
91        attr.parse_nested_meta(|meta| {
92            attr_keys!(
93                meta,
94                unknown = "unknown `copy` mode, expected `json`, `enum_str`, `with = \"fn\"`, \
95                    `rename = \"col\"`, or `skip`";
96                "rename" => (rename.is_some(), "duplicate `rename` copy key") {
97                    rename = Some(meta.value()?.parse()?);
98                },
99                "skip" => (skip, "duplicate `skip` copy key") {
100                    skip = true;
101                },
102                "json" => (
103                    mode.is_some(),
104                    DUPLICATE_BIND_MODE
105                ) {
106                    mode = Some(CopyBindMode::Json);
107                },
108                "enum_str" => (
109                    mode.is_some(),
110                    DUPLICATE_BIND_MODE
111                ) {
112                    mode = Some(CopyBindMode::EnumStr);
113                },
114                "with" => (
115                    mode.is_some(),
116                    DUPLICATE_BIND_MODE
117                ) {
118                    let lit: LitStr = meta.value()?.parse()?;
119
120                    mode = Some(CopyBindMode::With(lit.parse().map_err(|error| {
121                        syn::Error::new_spanned(
122                            &lit,
123                            format!("#[copy(with = \"...\")] expects a valid Rust path: {error}"),
124                        )
125                    })?));
126                }
127            )
128        })?;
129    }
130
131    if skip && (mode.is_some() || rename.is_some()) {
132        return Err(syn::Error::new_spanned(
133            field,
134            "`copy(skip)` cannot be combined with a bind mode or column rename",
135        ));
136    }
137
138    Ok(CopyFieldPlan {
139        ident,
140        skip,
141        mode: mode.unwrap_or(CopyBindMode::Default),
142        rename,
143    })
144}
145
146#[cfg(test)]
147mod tests {
148    use googletest::prelude::*;
149
150    use super::*;
151
152    #[gtest]
153    fn copy_field_rejects_skip_combined_with_binding_configuration() -> Result<()> {
154        let input: DeriveInput = syn::parse_quote! {
155            struct Row {
156                #[copy(skip, rename = "ignored")]
157                value: i32,
158            }
159        };
160        let field = match &input.data {
161            syn::Data::Struct(data) => data.fields.iter().next().or_fail()?,
162            _ => return fail!("fixture is a struct"),
163        };
164
165        let error = parse_copy_field(field).err().or_fail()?;
166
167        verify_that!(
168            error.to_string(),
169            eq("`copy(skip)` cannot be combined with a bind mode or column rename")
170        )
171    }
172}