wowlab_engine_macros_impl/
copy_insert.rs1use quote::quote;
2use syn::{Data, DeriveInput, Fields, LitStr};
3
4use crate::copy_insert_parse::{CopyBindMode, parse_copy_field, parse_copy_insert_container};
5
6pub(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}