Skip to main content

sz_rust_macros/
lib.rs

1//! SZ-Rust Macros — 过程宏包
2//!
3//! 提供 3 个过程宏:
4//!
5//! | 宏 | 类型 | 对齐 PHP | 实现阶段 |
6//! |----|------|---------|---------|
7//! | `#[controller]` | 属性宏 | 控制器声明 | ✅ |
8//! | `#[model]` | 属性宏 | 模型声明 | ✅ |
9//! | `compact!` | 函数式宏 | `compact()` | ✅ |
10//!
11//! `#[controller]` 自动实现 `SzController` trait。
12//! `#[model]` 自动实现 `Model` + `ModelExt` trait(基于字段式结构体)。
13//! `compact!` 对齐 PHP `compact()`。
14
15#![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// ============================================================================
25// #[controller] — 自动实现 SzController trait
26// ============================================================================
27
28/// `#[controller]` 属性宏
29///
30/// 为控制器结构体自动实现 `sz_rust_core::controller::SzController` trait。
31/// 由于 `SzController` 的所有方法都有默认实现,宏只需生成空 `impl` 块。
32///
33/// # 示例
34///
35/// ```ignore
36/// use sz_rust_macros::controller;
37///
38/// #[controller]
39/// pub struct UserController;
40/// ```
41///
42/// 生成代码等价于:
43///
44/// ```ignore
45/// impl ::sz_rust_core::controller::SzController for UserController {}
46/// ```
47#[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// ============================================================================
62// #[model] — 自动实现 Model + ModelExt trait
63// ============================================================================
64
65/// `#[model]` 属性宏
66///
67/// 为字段式结构体自动实现 `sz_orm_core::Model` + `sz_orm_core::ModelExt` trait。
68///
69/// # 属性参数
70///
71/// - `table = "表名"`:指定数据库表名(必填)
72/// - `pk = "主键列名"`:指定主键列名(默认 `"id"`)
73///
74/// # 字段类型支持
75///
76/// 宏根据字段类型自动生成 `Value` 转换代码:
77///
78/// | Rust 类型 | OrmValue 变体 |
79/// |----------|--------------|
80/// | `i64` | `Value::I64` |
81/// | `i32` | `Value::I32` |
82/// | `f64` | `Value::F64` |
83/// | `String` | `Value::String` |
84/// | `bool` | `Value::Bool` |
85///
86/// 不支持的字段类型会被跳过(不参与 `columns`/`get_column_value`/`from_value`)。
87///
88/// # 主键类型
89///
90/// 主键字段(由 `pk` 指定的列名对应的字段)的类型作为 `Model::PrimaryKey`。
91/// 主键字段必须为 `i64` 或 `i32`。
92///
93/// # fillable / guarded
94///
95/// - `guarded`:默认包含主键列名
96/// - `fillable`:默认包含所有非主键的已支持字段
97///
98/// # 示例
99///
100/// ```ignore
101/// use sz_rust_macros::model;
102///
103/// #[model(table = "users", pk = "user_id")]
104/// pub struct User {
105///     pub user_id: i64,
106///     pub name: String,
107///     pub age: i64,
108/// }
109/// ```
110#[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    // 解析属性参数:table = "xxx", pk = "xxx"
116    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    // 收集字段信息
129    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    // 主键字段类型必须是 i64 或 i32
139    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    // 生成 columns 列表
162    let column_names: Vec<&str> = fields.iter().map(|f| f.column_name.as_str()).collect();
163
164    // 生成 fillable 列表(非主键字段)
165    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    // 生成 get_column_value 的 match 分支
172    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    // 生成 from_value 的赋值代码
192    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
286/// `#[model]` 属性参数
287struct ModelAttr {
288    table: String,
289    pk: Option<String>,
290}
291
292/// 解析 `#[model(table = "xxx", pk = "xxx")]` 属性参数
293fn 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    // 解析为逗号分隔的 key = "value" 列表
299    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
324/// 辅助类型:解析 `key = "value"` 形式的属性参数
325struct 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
342/// 字段信息
343struct FieldInfo {
344    ident: Ident,
345    /// 类型字符串(用于比较,如 "i64"、"String")
346    ty: String,
347    /// 原始类型 token(用于代码生成)
348    ty_token: syn::Type,
349    column_name: String,
350}
351
352/// 收集结构体中支持的字段
353fn 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        // 跳过带 #[model(skip)] 标记的字段
369        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        // 提取类型字符串
381        let ty_str = extract_type_string(&field.ty);
382        let ty_token = field.ty.clone();
383
384        // column_name 默认为字段名
385        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
402/// 提取字段类型字符串
403///
404/// 支持的类型:i64, i32, f64, String, bool
405/// 其他类型返回原始字符串(get_column_value 中会返回 None)
406fn extract_type_string(ty: &syn::Type) -> String {
407    let s = quote!(#ty).to_string();
408    // 去除空白
409    s.split_whitespace().collect::<Vec<_>>().join(" ")
410}
411
412// ============================================================================
413// compact! — 函数式宏
414// ============================================================================
415
416/// `compact!` 函数式宏
417///
418/// 将变量名 → 值按声明顺序插入 `serde_json::Map<String, serde_json::Value>`,
419/// 严格对齐 PHP `compact()` 函数行为:
420///
421/// ## PHP `compact()` 行为
422///
423/// ```php
424/// $code = 1;
425/// $msg = "ok";
426/// $data = ["id" => 1];
427/// return compact('code', 'msg', 'data');
428/// // 等价于:['code' => 1, 'msg' => "ok", 'data' => ["id" => 1]]
429/// ```
430///
431/// ## Rust `compact!` 等价
432///
433/// ```
434/// use sz_rust_macros::compact;
435/// use serde_json::json;
436///
437/// let code = 1i32;
438/// let msg = "ok".to_string();
439/// let data = json!({"id": 1});
440/// let result = compact!(code, msg, data);
441/// assert_eq!(result.len(), 3);
442/// assert_eq!(result["code"], 1);
443/// assert_eq!(result["msg"], "ok");
444/// assert_eq!(result["data"]["id"], 1);
445/// ```
446///
447/// ## 字段顺序
448///
449/// 字段顺序严格按宏参数顺序保序(对齐 PHP `compact()` 参数顺序),
450/// 依赖 `serde_json::Map` 的 `preserve_order` feature(默认启用)。
451///
452/// ## 类型转换
453///
454/// 使用 `serde_json::to_value()` 将变量值转换为 `serde_json::Value`,
455/// 支持所有实现 `serde::Serialize` 的类型。
456///
457/// # 示例
458///
459/// ```
460/// use sz_rust_macros::compact;
461///
462/// let name = "alice";
463/// let age = 30i32;
464/// let map = compact!(name, age);
465/// assert_eq!(map["name"], "alice");
466/// assert_eq!(map["age"], 30);
467/// ```
468#[proc_macro]
469pub fn compact(input: TokenStream) -> TokenStream {
470    // 解析输入为逗号分隔的标识符列表
471    // 对齐 PHP compact('var1', 'var2', ...) 参数语法
472    let names =
473        syn::parse_macro_input!(input with Punctuated::<Ident, Token![,]>::parse_terminated);
474
475    // 为每个标识符生成 map.insert 代码
476    // 使用 serde_json::to_value() 支持所有实现 serde::Serialize 的类型
477    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}