wowlab_engine_macros_impl/
copy_insert_parse.rs1use 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}