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}