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(
42                &input.ident,
43                "RustioAdmin requires named fields",
44            )
45            .to_compile_error()
46            .into();
47        }
48    };
49
50    let mut fields: Vec<FieldInfo> = Vec::new();
51    for f in &named.named {
52        let ident = f.ident.clone().expect("named field");
53        let name_str = ident.to_string();
54        let kind = match classify_type(&f.ty) {
55            Some(k) => k,
56            None => {
57                return syn::Error::new_spanned(
58                    &f.ty,
59                    "RustioAdmin: unsupported field type (supported: i32, i64, String, bool)",
60                )
61                .to_compile_error()
62                .into();
63            }
64        };
65        let editable = name_str != "id";
66        fields.push(FieldInfo {
67            ident,
68            name_str,
69            kind,
70            editable,
71        });
72    }
73
74    let admin_name = format!("{}s", name.to_string().to_lowercase());
75    let display_name = format!("{}s", name);
76
77    let field_entries: Vec<TokenStream2> = fields
78        .iter()
79        .map(|f| {
80            let n = &f.name_str;
81            let kind_token = kind_token(f.kind);
82            let editable = f.editable;
83            quote! {
84                ::rustio_core::admin::AdminField {
85                    name: #n,
86                    ty: #kind_token,
87                    editable: #editable,
88                }
89            }
90        })
91        .collect();
92
93    let display_arms: Vec<TokenStream2> = fields
94        .iter()
95        .map(|f| {
96            let ident = &f.ident;
97            let name_str = &f.name_str;
98            quote! {
99                #name_str => Some(self.#ident.to_string()),
100            }
101        })
102        .collect();
103
104    let from_form_assignments: Vec<TokenStream2> = fields
105        .iter()
106        .map(|f| from_form_assignment(f))
107        .collect();
108
109    let expanded = quote! {
110        impl ::rustio_core::admin::AdminModel for #name {
111            const ADMIN_NAME: &'static str = #admin_name;
112            const DISPLAY_NAME: &'static str = #display_name;
113            const FIELDS: &'static [::rustio_core::admin::AdminField] = &[
114                #( #field_entries ),*
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 classify_type(ty: &Type) -> Option<FieldKind> {
139    if let Type::Path(syn::TypePath { path, .. }) = ty {
140        if let Some(last) = path.segments.last() {
141            let ident = last.ident.to_string();
142            return match ident.as_str() {
143                "i32" => Some(FieldKind::I32),
144                "i64" => Some(FieldKind::I64),
145                "String" => Some(FieldKind::String),
146                "bool" => Some(FieldKind::Bool),
147                _ => None,
148            };
149        }
150    }
151    None
152}
153
154fn kind_token(kind: FieldKind) -> TokenStream2 {
155    match kind {
156        FieldKind::I32 => quote! { ::rustio_core::admin::FieldType::I32 },
157        FieldKind::I64 => quote! { ::rustio_core::admin::FieldType::I64 },
158        FieldKind::String => quote! { ::rustio_core::admin::FieldType::String },
159        FieldKind::Bool => quote! { ::rustio_core::admin::FieldType::Bool },
160    }
161}
162
163fn from_form_assignment(f: &FieldInfo) -> TokenStream2 {
164    let ident = &f.ident;
165    let name_str = &f.name_str;
166    if !f.editable {
167        return quote! { #ident: id.unwrap_or(0), };
168    }
169    match f.kind {
170        FieldKind::String => quote! {
171            #ident: form.get(#name_str).unwrap_or("").to_owned(),
172        },
173        FieldKind::Bool => quote! {
174            #ident: matches!(form.get(#name_str), Some(v) if v == "on" || v == "true"),
175        },
176        FieldKind::I64 => quote! {
177            #ident: form
178                .get(#name_str)
179                .unwrap_or("0")
180                .parse::<i64>()
181                .map_err(|_| ::rustio_core::Error::BadRequest(
182                    format!("invalid integer for field `{}`", #name_str)
183                ))?,
184        },
185        FieldKind::I32 => quote! {
186            #ident: form
187                .get(#name_str)
188                .unwrap_or("0")
189                .parse::<i32>()
190                .map_err(|_| ::rustio_core::Error::BadRequest(
191                    format!("invalid integer for field `{}`", #name_str)
192                ))?,
193        },
194    }
195}