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}