Skip to main content

rustio_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::quote;
4use syn::{parse_macro_input, Data, DeriveInput, Fields, Type};
5
6#[derive(Clone, Copy)]
7enum FieldKind {
8    I32,
9    I64,
10    String,
11    Bool,
12}
13
14struct FieldInfo {
15    ident: syn::Ident,
16    name_str: String,
17    kind: FieldKind,
18    editable: bool,
19}
20
21#[proc_macro_derive(RustioAdmin)]
22pub fn derive_rustio_admin(input: TokenStream) -> TokenStream {
23    let input = parse_macro_input!(input as DeriveInput);
24    let name = &input.ident;
25
26    let data = match &input.data {
27        Data::Struct(d) => d,
28        _ => {
29            return syn::Error::new_spanned(
30                &input.ident,
31                "RustioAdmin only supports structs with named fields",
32            )
33            .to_compile_error()
34            .into();
35        }
36    };
37
38    let named = match &data.fields {
39        Fields::Named(n) => n,
40        _ => {
41            return syn::Error::new_spanned(&input.ident, "RustioAdmin requires named fields")
42                .to_compile_error()
43                .into();
44        }
45    };
46
47    let mut fields: Vec<FieldInfo> = Vec::new();
48    for f in &named.named {
49        let ident = f.ident.clone().expect("named field");
50        let name_str = ident.to_string();
51        let kind = match classify_type(&f.ty) {
52            Some(k) => k,
53            None => {
54                return syn::Error::new_spanned(
55                    &f.ty,
56                    "RustioAdmin: unsupported field type (supported: i32, i64, String, bool)",
57                )
58                .to_compile_error()
59                .into();
60            }
61        };
62        let editable = name_str != "id";
63        fields.push(FieldInfo {
64            ident,
65            name_str,
66            kind,
67            editable,
68        });
69    }
70
71    let admin_name = pluralize(&name.to_string().to_lowercase());
72    let display_name = pluralize(&name.to_string());
73    let singular_name = singularize(&name.to_string());
74
75    let field_entries: Vec<TokenStream2> = fields
76        .iter()
77        .map(|f| {
78            let n = &f.name_str;
79            let kind_token = kind_token(f.kind);
80            let editable = f.editable;
81            quote! {
82                ::rustio_core::admin::AdminField {
83                    name: #n,
84                    ty: #kind_token,
85                    editable: #editable,
86                }
87            }
88        })
89        .collect();
90
91    let display_arms: Vec<TokenStream2> = fields
92        .iter()
93        .map(|f| {
94            let ident = &f.ident;
95            let name_str = &f.name_str;
96            quote! {
97                #name_str => Some(self.#ident.to_string()),
98            }
99        })
100        .collect();
101
102    let from_form_assignments: Vec<TokenStream2> =
103        fields.iter().map(from_form_assignment).collect();
104
105    let expanded = quote! {
106        impl ::rustio_core::admin::AdminModel for #name {
107            const ADMIN_NAME: &'static str = #admin_name;
108            const DISPLAY_NAME: &'static str = #display_name;
109            const FIELDS: &'static [::rustio_core::admin::AdminField] = &[
110                #( #field_entries ),*
111            ];
112
113            fn singular_name() -> &'static str {
114                #singular_name
115            }
116
117            fn field_display(&self, name: &str) -> Option<String> {
118                match name {
119                    #( #display_arms )*
120                    _ => None,
121                }
122            }
123
124            fn from_form(
125                form: &::rustio_core::admin::FormData,
126                id: Option<i64>,
127            ) -> Result<Self, ::rustio_core::Error> {
128                Ok(Self {
129                    #( #from_form_assignments )*
130                })
131            }
132        }
133    };
134
135    expanded.into()
136}
137
138fn pluralize(name: &str) -> String {
139    if name.ends_with('s') {
140        name.to_string()
141    } else {
142        format!("{name}s")
143    }
144}
145
146fn singularize(name: &str) -> String {
147    if let Some(stripped) = name.strip_suffix('s') {
148        if !stripped.is_empty() {
149            return stripped.to_string();
150        }
151    }
152    name.to_string()
153}
154
155fn classify_type(ty: &Type) -> Option<FieldKind> {
156    if let Type::Path(syn::TypePath { path, .. }) = ty {
157        if let Some(last) = path.segments.last() {
158            let ident = last.ident.to_string();
159            return match ident.as_str() {
160                "i32" => Some(FieldKind::I32),
161                "i64" => Some(FieldKind::I64),
162                "String" => Some(FieldKind::String),
163                "bool" => Some(FieldKind::Bool),
164                _ => None,
165            };
166        }
167    }
168    None
169}
170
171fn kind_token(kind: FieldKind) -> TokenStream2 {
172    match kind {
173        FieldKind::I32 => quote! { ::rustio_core::admin::FieldType::I32 },
174        FieldKind::I64 => quote! { ::rustio_core::admin::FieldType::I64 },
175        FieldKind::String => quote! { ::rustio_core::admin::FieldType::String },
176        FieldKind::Bool => quote! { ::rustio_core::admin::FieldType::Bool },
177    }
178}
179
180fn from_form_assignment(f: &FieldInfo) -> TokenStream2 {
181    let ident = &f.ident;
182    let name_str = &f.name_str;
183    if !f.editable {
184        return quote! { #ident: id.unwrap_or(0), };
185    }
186    match f.kind {
187        FieldKind::String => quote! {
188            #ident: {
189                let v = form.get(#name_str).unwrap_or("").trim();
190                if v.is_empty() {
191                    return Err(::rustio_core::Error::BadRequest(
192                        format!("field `{}` is required", #name_str)
193                    ));
194                }
195                v.to_owned()
196            },
197        },
198        FieldKind::Bool => quote! {
199            #ident: matches!(form.get(#name_str), Some(v) if v == "on" || v == "true"),
200        },
201        FieldKind::I64 => quote! {
202            #ident: {
203                let raw = form.get(#name_str).unwrap_or("").trim();
204                if raw.is_empty() {
205                    return Err(::rustio_core::Error::BadRequest(
206                        format!("field `{}` is required", #name_str)
207                    ));
208                }
209                raw.parse::<i64>().map_err(|_| ::rustio_core::Error::BadRequest(
210                    format!("field `{}` must be a valid integer", #name_str)
211                ))?
212            },
213        },
214        FieldKind::I32 => quote! {
215            #ident: {
216                let raw = form.get(#name_str).unwrap_or("").trim();
217                if raw.is_empty() {
218                    return Err(::rustio_core::Error::BadRequest(
219                        format!("field `{}` is required", #name_str)
220                    ));
221                }
222                raw.parse::<i32>().map_err(|_| ::rustio_core::Error::BadRequest(
223                    format!("field `{}` must be a valid integer", #name_str)
224                ))?
225            },
226        },
227    }
228}