Skip to main content

wowlab_engine_macros_impl/
copy_insert.rs

1use quote::quote;
2use syn::{Data, DeriveInput, Fields, LitStr};
3
4use crate::copy_insert_parse::{CopyBindMode, parse_copy_field, parse_copy_insert_container};
5
6// #t(fn: rust_alloc_in_loop) generated COPY column names must be owned by the emitted token stream.
7pub(crate) fn expand_copy_insert(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
8    let Data::Struct(data_struct) = &input.data else {
9        return Err(syn::Error::new_spanned(
10            &input.ident,
11            "#[derive(CopyInsert)] can only be applied to structs",
12        ));
13    };
14    let Fields::Named(named) = &data_struct.fields else {
15        return Err(syn::Error::new_spanned(
16            &input.ident,
17            "#[derive(CopyInsert)] requires a struct with named fields",
18        ));
19    };
20
21    let container = parse_copy_insert_container(input)?;
22    let support = &container.support;
23
24    let capacity = named.named.len() + 1;
25    let mut columns: Vec<LitStr> = Vec::with_capacity(capacity);
26    let mut binds: Vec<proc_macro2::TokenStream> = Vec::with_capacity(capacity);
27
28    for field in &named.named {
29        let plan = parse_copy_field(field)?;
30
31        if plan.skip {
32            continue;
33        }
34
35        let ident = &plan.ident;
36        let column = plan
37            .rename
38            .unwrap_or_else(|| LitStr::new(&ident.to_string(), ident.span()));
39        let bind = match &plan.mode {
40            CopyBindMode::Default => quote! { w.push_bind(&self.#ident); },
41            CopyBindMode::Json => quote! { w.push_bind(#support::to_json(&self.#ident)); },
42            CopyBindMode::EnumStr => quote! { w.push_bind(self.#ident.as_str()); },
43            CopyBindMode::With(func) => quote! { w.push_bind(#func(self)); },
44        };
45
46        columns.push(column);
47        binds.push(bind);
48    }
49
50    if columns.is_empty() && !container.patch_first {
51        return Err(syn::Error::new_spanned(
52            &input.ident,
53            "CopyInsert's default patch position requires at least one non-skipped field; \
54             use #[copy_insert(patch_first)] when the patch is the first column",
55        ));
56    }
57
58    let patch_idx = usize::from(!container.patch_first);
59
60    columns.insert(
61        patch_idx,
62        LitStr::new("patch_version", proc_macro2::Span::call_site()),
63    );
64    binds.insert(patch_idx, quote! { w.push_bind(patch); });
65
66    let ident = &input.ident;
67    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
68
69    Ok(quote! {
70        impl #impl_generics #support::CopyInsert for #ident #type_generics #where_clause {
71            const COLUMNS: &'static [&'static str] = &[ #(#columns),* ];
72            fn copy_row(&self, w: &mut #support::CopyRow, patch: &str) {
73                #(#binds)*
74            }
75        }
76    })
77}
78
79#[cfg(test)]
80mod tests {
81    use googletest::prelude::*;
82
83    use super::*;
84
85    #[gtest]
86    fn default_injects_patch_at_index_one_and_maps_modes() -> Result<()> {
87        let input: DeriveInput = syn::parse_quote! {
88            struct Row {
89                id: i32,
90                name: String,
91                #[copy(json)]
92                data: Vec<i32>,
93                #[copy(rename = "type")]
94                kind: i32,
95                #[copy(skip)]
96                ignored: i32,
97            }
98        };
99        let out = expand_copy_insert(&input).or_fail()?;
100        let expected = quote! {
101            impl crate::copy::CopyInsert for Row {
102                const COLUMNS: &'static [&'static str] =
103                    &["id", "patch_version", "name", "data", "type"];
104                fn copy_row(&self, w: &mut crate::copy::CopyRow, patch: &str) {
105                    w.push_bind(&self.id);
106                    w.push_bind(patch);
107                    w.push_bind(&self.name);
108                    w.push_bind(crate::copy::to_json(&self.data));
109                    w.push_bind(&self.kind);
110                }
111            }
112        };
113
114        verify_that!(out.to_string(), eq(&expected.to_string()))
115    }
116
117    #[gtest]
118    fn patch_first_injects_at_index_zero_and_crate_override() -> Result<()> {
119        let input: DeriveInput = syn::parse_quote! {
120            #[copy_insert(patch_first, crate = "wowlab_types::copy")]
121            struct Row {
122                #[copy(skip)]
123                id: i32,
124                value: i32,
125                #[copy(enum_str)]
126                kind: Kind,
127                #[copy(with = "value_col")]
128                extra: i32,
129            }
130        };
131        let out = expand_copy_insert(&input).or_fail()?;
132        let expected = quote! {
133            impl wowlab_types::copy::CopyInsert for Row {
134                const COLUMNS: &'static [&'static str] =
135                    &["patch_version", "value", "kind", "extra"];
136                fn copy_row(&self, w: &mut wowlab_types::copy::CopyRow, patch: &str) {
137                    w.push_bind(patch);
138                    w.push_bind(&self.value);
139                    w.push_bind(self.kind.as_str());
140                    w.push_bind(value_col(self));
141                }
142            }
143        };
144
145        verify_that!(out.to_string(), eq(&expected.to_string()))
146    }
147
148    #[gtest]
149    fn generic_input_preserves_impl_generics_and_where_clause() -> Result<()> {
150        let input: DeriveInput = syn::parse_quote! {
151            struct Row<T>
152            where
153                T: Copy,
154            {
155                id: i32,
156                value: T,
157            }
158        };
159
160        let output = expand_copy_insert(&input).or_fail()?;
161        let expected = quote! {
162            impl<T> crate::copy::CopyInsert for Row<T>
163            where
164                T: Copy,
165            {
166                const COLUMNS: &'static [&'static str] = &["id", "patch_version", "value"];
167                fn copy_row(&self, w: &mut crate::copy::CopyRow, patch: &str) {
168                    w.push_bind(&self.id);
169                    w.push_bind(patch);
170                    w.push_bind(&self.value);
171                }
172            }
173        };
174
175        verify_that!(output.to_string(), eq(&expected.to_string()))
176    }
177
178    #[gtest]
179    fn empty_default_position_returns_a_compile_diagnostic_instead_of_panicking() -> Result<()> {
180        let input: DeriveInput = syn::parse_quote! {
181            struct EmptyRow {}
182        };
183
184        let error = expand_copy_insert(&input).err().or_fail()?;
185
186        verify_that!(
187            error.to_string(),
188            eq(
189                "CopyInsert's default patch position requires at least one non-skipped field; \
190                use #[copy_insert(patch_first)] when the patch is the first column"
191            )
192        )
193    }
194
195    #[gtest]
196    fn patch_first_supports_a_patch_only_row() -> Result<()> {
197        let input: DeriveInput = syn::parse_quote! {
198            #[copy_insert(patch_first)]
199            struct PatchOnly {}
200        };
201
202        let output = expand_copy_insert(&input).or_fail()?;
203        let expected = quote! {
204            impl crate::copy::CopyInsert for PatchOnly {
205                const COLUMNS: &'static [&'static str] = &["patch_version"];
206                fn copy_row(&self, w: &mut crate::copy::CopyRow, patch: &str) {
207                    w.push_bind(patch);
208                }
209            }
210        };
211
212        verify_that!(output.to_string(), eq(&expected.to_string()))
213    }
214}