1#![forbid(unsafe_code)]
16#![warn(missing_docs)]
17
18use proc_macro::TokenStream;
19use quote::quote;
20use syn::parse::Parser;
21use syn::punctuated::Punctuated;
22use syn::{Ident, ItemStruct, LitStr, Token};
23
24#[proc_macro_attribute]
48pub fn controller(_attr: TokenStream, item: TokenStream) -> TokenStream {
49 let input = syn::parse_macro_input!(item as ItemStruct);
50 let struct_name = &input.ident;
51
52 let expanded = quote! {
53 #input
54
55 impl ::sz_rust_core::controller::SzController for #struct_name {}
56 };
57
58 expanded.into()
59}
60
61#[proc_macro_attribute]
111pub fn model(attr: TokenStream, item: TokenStream) -> TokenStream {
112 let input = syn::parse_macro_input!(item as ItemStruct);
113 let struct_name = &input.ident;
114
115 let args = match parse_model_attr(attr) {
117 Ok(args) => args,
118 Err(msg) => {
119 return syn::Error::new_spanned(&input, msg)
120 .to_compile_error()
121 .into();
122 }
123 };
124
125 let table_name = args.table;
126 let pk_name = args.pk.unwrap_or_else(|| "id".to_string());
127
128 let fields = match collect_fields(&input, &pk_name) {
130 Ok(fields) => fields,
131 Err(msg) => {
132 return syn::Error::new_spanned(&input, msg)
133 .to_compile_error()
134 .into();
135 }
136 };
137
138 let pk_field = fields
140 .iter()
141 .find(|f| f.column_name == pk_name)
142 .ok_or_else(|| {
143 format!(
144 "primary key field '{}' not found in struct '{}'",
145 pk_name, struct_name
146 )
147 });
148
149 let pk_field = match pk_field {
150 Ok(f) => f,
151 Err(msg) => {
152 return syn::Error::new_spanned(&input, msg)
153 .to_compile_error()
154 .into();
155 }
156 };
157
158 let pk_ty = pk_field.ty_token.clone();
159 let pk_ident = pk_field.ident.clone();
160
161 let column_names: Vec<&str> = fields.iter().map(|f| f.column_name.as_str()).collect();
163
164 let fillable_names: Vec<&str> = fields
166 .iter()
167 .filter(|f| f.column_name != pk_name)
168 .map(|f| f.column_name.as_str())
169 .collect();
170
171 let get_column_value_arms = fields.iter().map(|f| {
173 let col = &f.column_name;
174 let ident = &f.ident;
175 let ty = &f.ty;
176 if ty == "i64" {
177 quote! { #col => Some(::sz_orm_core::Value::I64(self.#ident)) }
178 } else if ty == "i32" {
179 quote! { #col => Some(::sz_orm_core::Value::I32(self.#ident)) }
180 } else if ty == "f64" {
181 quote! { #col => Some(::sz_orm_core::Value::F64(self.#ident)) }
182 } else if ty == "String" {
183 quote! { #col => Some(::sz_orm_core::Value::String(self.#ident.clone())) }
184 } else if ty == "bool" {
185 quote! { #col => Some(::sz_orm_core::Value::Bool(self.#ident)) }
186 } else {
187 quote! { #col => None }
188 }
189 });
190
191 let from_value_stmts = fields.iter().filter_map(|f| {
193 let col = &f.column_name;
194 let ident = &f.ident;
195 let ty = &f.ty;
196 if ty == "i64" {
197 Some(quote! {
198 if let Some(::sz_orm_core::Value::I64(v)) = map.get(#col) {
199 self.#ident = *v;
200 }
201 })
202 } else if ty == "i32" {
203 Some(quote! {
204 if let Some(::sz_orm_core::Value::I32(v)) = map.get(#col) {
205 self.#ident = *v;
206 }
207 })
208 } else if ty == "f64" {
209 Some(quote! {
210 if let Some(::sz_orm_core::Value::F64(v)) = map.get(#col) {
211 self.#ident = *v;
212 }
213 })
214 } else if ty == "String" {
215 Some(quote! {
216 if let Some(::sz_orm_core::Value::String(v)) = map.get(#col) {
217 self.#ident = v.clone();
218 }
219 })
220 } else if ty == "bool" {
221 Some(quote! {
222 if let Some(::sz_orm_core::Value::Bool(v)) = map.get(#col) {
223 self.#ident = *v;
224 }
225 })
226 } else {
227 None
228 }
229 });
230
231 let table_name_lit = LitStr::new(&table_name, proc_macro2::Span::call_site());
232 let pk_name_lit = LitStr::new(&pk_name, proc_macro2::Span::call_site());
233
234 let expanded = quote! {
235 #input
236
237 impl ::sz_orm_core::Model for #struct_name {
238 type PrimaryKey = #pk_ty;
239
240 fn table_name() -> &'static str {
241 #table_name_lit
242 }
243
244 fn pk_name() -> &'static str {
245 #pk_name_lit
246 }
247
248 fn pk(&self) -> Self::PrimaryKey {
249 self.#pk_ident.clone()
250 }
251
252 fn set_pk(&mut self, pk: Self::PrimaryKey) {
253 self.#pk_ident = pk;
254 }
255 }
256
257 impl ::sz_orm_core::ModelExt for #struct_name {
258 fn columns() -> Vec<&'static str> {
259 vec![#(#column_names),*]
260 }
261
262 fn fillable() -> Vec<&'static str> {
263 vec![#(#fillable_names),*]
264 }
265
266 fn guarded() -> Vec<&'static str> {
267 vec![#pk_name_lit]
268 }
269
270 fn get_column_value(&self, column: &str) -> Option<::sz_orm_core::Value> {
271 match column {
272 #(#get_column_value_arms,)*
273 _ => None,
274 }
275 }
276
277 fn from_value(&mut self, map: std::collections::HashMap<String, ::sz_orm_core::Value>) {
278 #(#from_value_stmts)*
279 }
280 }
281 };
282
283 expanded.into()
284}
285
286struct ModelAttr {
288 table: String,
289 pk: Option<String>,
290}
291
292fn parse_model_attr(attr: TokenStream) -> Result<ModelAttr, String> {
294 if attr.is_empty() {
295 return Err("missing required 'table' attribute: #[model(table = \"xxx\")]".to_string());
296 }
297
298 let attr2: proc_macro2::TokenStream = attr.into();
300 let meta_list = Punctuated::<MetaNameValueStr, Token![,]>::parse_terminated
301 .parse2(attr2)
302 .map_err(|e| format!("failed to parse model attributes: {e}"))?;
303
304 let mut table = None;
305 let mut pk = None;
306
307 for nv in meta_list {
308 let key = nv.key.to_string();
309 let value = nv.value;
310 match key.as_str() {
311 "table" => table = Some(value),
312 "pk" => pk = Some(value),
313 _ => return Err(format!("unknown model attribute '{}'", key)),
314 }
315 }
316
317 let table = table.ok_or_else(|| {
318 "missing required 'table' attribute: #[model(table = \"xxx\")]".to_string()
319 })?;
320
321 Ok(ModelAttr { table, pk })
322}
323
324struct MetaNameValueStr {
326 key: Ident,
327 value: String,
328}
329
330impl syn::parse::Parse for MetaNameValueStr {
331 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
332 let key: Ident = input.parse()?;
333 let _: Token![=] = input.parse()?;
334 let value: LitStr = input.parse()?;
335 Ok(Self {
336 key,
337 value: value.value(),
338 })
339 }
340}
341
342struct FieldInfo {
344 ident: Ident,
345 ty: String,
347 ty_token: syn::Type,
349 column_name: String,
350}
351
352fn collect_fields(input: &ItemStruct, _pk_name: &str) -> Result<Vec<FieldInfo>, String> {
354 let fields = match &input.fields {
355 syn::Fields::Named(named) => &named.named,
356 _ => {
357 return Err("#[model] only supports structs with named fields".to_string());
358 }
359 };
360
361 let mut result = Vec::new();
362 for field in fields {
363 let ident = field
364 .ident
365 .clone()
366 .ok_or_else(|| "#[model] requires all fields to be named".to_string())?;
367
368 if field.attrs.iter().any(|attr| {
370 attr.path().is_ident("model")
371 && attr
372 .parse_args::<syn::Ident>()
373 .ok()
374 .map(|i| i == "skip")
375 .unwrap_or(false)
376 }) {
377 continue;
378 }
379
380 let ty_str = extract_type_string(&field.ty);
382 let ty_token = field.ty.clone();
383
384 let column_name = ident.to_string();
386
387 result.push(FieldInfo {
388 ident,
389 ty: ty_str,
390 ty_token,
391 column_name,
392 });
393 }
394
395 if result.is_empty() {
396 return Err("#[model] struct must have at least one field".to_string());
397 }
398
399 Ok(result)
400}
401
402fn extract_type_string(ty: &syn::Type) -> String {
407 let s = quote!(#ty).to_string();
408 s.split_whitespace().collect::<Vec<_>>().join(" ")
410}
411
412#[proc_macro]
469pub fn compact(input: TokenStream) -> TokenStream {
470 let names =
473 syn::parse_macro_input!(input with Punctuated::<Ident, Token![,]>::parse_terminated);
474
475 let inserts = names.iter().map(|name| {
478 let name_str = name.to_string();
479 quote! {
480 map.insert(
481 #name_str.to_string(),
482 serde_json::to_value(&#name).unwrap_or(serde_json::Value::Null),
483 );
484 }
485 });
486
487 quote! {
488 {
489 let mut map: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
490 #(#inserts)*
491 map
492 }
493 }
494 .into()
495}