Skip to main content

scalar_derive/
lib.rs

1use convert_case::Casing;
2use darling::{util::Flag, FromDeriveInput, FromField, FromVariant};
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{parse_macro_input, Data, DeriveInput, Ident};
6
7#[derive(FromDeriveInput)]
8#[darling(attributes(document), supports(struct_named))]
9struct Document {
10    identifier: Option<String>,
11    title: Option<String>,
12    singleton: Flag,
13}
14
15#[derive(FromDeriveInput)]
16#[darling(supports(struct_newtype, struct_named))]
17#[darling(attributes(field))]
18struct ToEditorField {
19    ident: syn::Ident,
20    generics: syn::Generics,
21    data: darling::ast::Data<(), FieldInfo>,
22    editor_component: Option<String>,
23}
24
25#[derive(FromDeriveInput)]
26#[darling(supports(enum_unit, enum_named))]
27struct Enum {
28    data: darling::ast::Data<EnumVariant, FieldInfo>,
29}
30
31#[derive(FromVariant)]
32struct EnumVariant {
33    ident: Ident,
34    fields: darling::ast::Fields<FieldInfo>,
35}
36
37#[derive(FromField, Clone)]
38#[darling(attributes(field))]
39struct FieldInfo {
40    ident: Option<syn::Ident>,
41    ty: syn::Type,
42    title: Option<String>,
43    placeholder: Option<String>,
44    editor_component: Option<String>,
45    default: Option<syn::Lit>,
46    label: Flag,
47    sublabel: Flag,
48}
49
50#[derive(FromField, Clone)]
51#[darling(attributes(validate))]
52struct ValidateInfo {
53    ident: Option<syn::Ident>,
54    skip: Flag,
55    with: Option<Ident>,
56}
57
58/// Sets up an enum for use in a Document. This macro does a couple of things:
59/// 1. It derives serde's Serialize and Deserialize traits. Make sure you have serde installed!
60/// 2. Sets up said serialization and deserialization to work the way the editor expects.
61/// 3. Derives `ToEditorField` for the schema
62#[proc_macro_attribute]
63pub fn doc_enum(_metadata: TokenStream, input: TokenStream) -> TokenStream {
64    let input: proc_macro2::TokenStream = input.into();
65    let output = quote! {
66        #[derive(::serde::Serialize, ::serde::Deserialize, ::scalar_cms::Enum)]
67        #[serde(tag = "type")]
68        #input
69    };
70    output.into()
71}
72
73/// Derives `EditorField`.
74///
75/// # Panics
76///
77/// Panics if the input isn't a struct somehow.
78#[proc_macro_derive(EditorField, attributes(field))]
79pub fn struct_to_editor_field(input: TokenStream) -> TokenStream {
80    let input: DeriveInput = parse_macro_input!(input);
81    let struct_info = match ToEditorField::from_derive_input(&input) {
82        Ok(v) => v,
83        Err(e) => return TokenStream::from(e.write_errors()),
84    };
85
86    let ident = struct_info.ident;
87    let fields = struct_info
88        .data
89        .take_struct()
90        .expect("a compiler error should've been returned, this has to be a struct");
91
92    let component_key = if let Some(str) = struct_info.editor_component {
93        quote! { Some(#str.into()) }
94    } else {
95        quote! { None }
96    };
97
98    match fields.style {
99        darling::ast::Style::Tuple => {
100            let field = fields
101                .fields
102                .first()
103                .expect("there should always be at least one field");
104            let field_ty = &field.ty;
105            quote! {
106                impl ::scalar_cms::editor_field::ToEditorField for #ident  {
107                    fn to_editor_field(
108                        default: Option<impl Into<#ident>>,
109                        name: &'static str,
110                        title: &'static str,
111                        placeholder: Option<&'static str>,
112                        validator: Option<&'static str>,
113                        component_key: Option<&'static str>
114                    ) -> ::scalar_cms::EditorField
115                    where
116                        Self: std::marker::Sized,
117                    {
118                        use ::scalar_cms::editor_field::ToEditorField;
119                        <#field_ty>::to_editor_field(default.map(Into::into), name, title, placeholder, validator, component_key.or(#component_key))
120                    }
121                }
122
123                impl From<#ident> for #field_ty {
124                    fn from(val: #ident) -> Self {
125                        val.0
126                    }
127                }
128            }
129            .into()
130        }
131        darling::ast::Style::Struct => {
132            let fields = fields
133                .iter()
134                .map(|f| field_to_info_call(f.to_owned()))
135                .collect::<Vec<_>>();
136
137            let (impl_generics, ty_generics, where_clause) = struct_info.generics.split_for_impl();
138            let ty = quote! { #ident #ty_generics };
139
140            quote! {
141                impl #impl_generics ::scalar_cms::editor_field::ToEditorField for #ty where #ty: ::serde::Serialize #where_clause {
142                    fn to_editor_field(
143                        default: Option<impl Into<#ty>>,
144                        name: &'static str,
145                        title: &'static str,
146                        placeholder: Option<&'static str>,
147                        validator: Option<&'static str>,
148                        component_key: Option<&'static str>
149                    ) -> ::scalar_cms::EditorField
150                    where
151                        Self: std::marker::Sized,
152                    {
153                        ::scalar_cms::EditorField {
154                            name,
155                            title,
156                            placeholder,
157                            required: true,
158                            validator,
159                            field_type: ::scalar_cms::EditorType::Struct {
160                                default: default.map(Into::into).as_ref().map(::scalar_cms::serde_json::to_value).map(|v| v.expect("a struct that should serialize to json")),
161                                component_key: component_key.map(Into::into).or(#component_key),
162                                fields: vec![#(#fields),*]
163                            }
164                        }
165                    }
166                }
167            }
168            .into()
169        }
170        darling::ast::Style::Unit => unreachable!("it's impossible for this to be a unit struct"),
171    }
172}
173
174#[proc_macro_derive(Enum)]
175pub fn derive_enum(input: TokenStream) -> TokenStream {
176    let input: DeriveInput = parse_macro_input!(input);
177    let enum_info = match Enum::from_derive_input(&input) {
178        Ok(v) => v,
179        Err(e) => return TokenStream::from(e.write_errors()),
180    };
181    let ident = input.ident;
182
183    let variants: Vec<proc_macro2::TokenStream> = match enum_info.data {
184        darling::ast::Data::Enum(variants) => variants
185            .iter()
186            .map(|v| {
187                let ident = v.ident.to_string();
188                let fields: Vec<proc_macro2::TokenStream> = v
189                    .fields
190                    .iter()
191                    .map(|field| field_to_info_call(field.to_owned()))
192                    .collect();
193
194                let fields_tokens = if fields.is_empty() {
195                    quote! { None }
196                } else {
197                    quote! { Some(vec![#(#fields),*]) }
198                };
199
200                quote! {
201                    ::scalar_cms::editor_type::EnumVariant {
202                        variant_name: #ident,
203                        fields: #fields_tokens
204                    }
205                }
206            })
207            .collect(),
208        darling::ast::Data::Struct(_) => unreachable!(),
209    };
210
211    let output = quote! {
212        impl ::scalar_cms::editor_field::ToEditorField for #ident where Self: ::serde::Serialize {
213            fn to_editor_field(default: Option<impl Into<Self>>, name: &'static str, title: &'static str, placeholder: Option<&'static str>, validator: Option<&'static str>, component_key: Option<&'static str>) -> ::scalar_cms::EditorField where Self: std::marker::Sized {
214                ::scalar_cms::EditorField { name, title, placeholder, required: true, validator, field_type: ::scalar_cms::EditorType::Enum {
215                    default: default.map(Into::into).map(::scalar_cms::serde_json::to_value).map(|v| v.expect("a struct that should serialize to json")),
216                    component_key: component_key.map(Into::into),
217                    variants: vec![#(#variants),*]
218                } }
219            }
220        }
221    };
222    output.into()
223}
224
225/// Derives the document trait.
226///
227/// # Panics
228///
229/// Panics if the input is somehow a tuple struct that isn't caught.
230#[proc_macro_derive(Document, attributes(document, field, validate))]
231pub fn derive_document(input: TokenStream) -> TokenStream {
232    let input: DeriveInput = parse_macro_input!(input);
233    let document = match Document::from_derive_input(&input) {
234        Ok(v) => v,
235        Err(e) => {
236            return TokenStream::from(e.write_errors());
237        }
238    };
239    let struct_fields = match input.data {
240        Data::Struct(st) => st.fields,
241        _ => unreachable!(),
242    };
243    let ident = input.ident;
244
245    let doc_identifier = document
246        .identifier
247        .unwrap_or_else(|| ident.to_string().to_case(convert_case::Case::Snake));
248
249    let doc_title = document
250        .title
251        .unwrap_or_else(|| ident.to_string().to_case(convert_case::Case::Title));
252
253    let singleton = document.singleton.is_present();
254
255    let struct_field_infos = match struct_fields
256        .iter()
257        .map(FieldInfo::from_field)
258        .collect::<Result<Vec<FieldInfo>, darling::Error>>()
259    {
260        Ok(f) => f,
261        Err(e) => return TokenStream::from(e.write_errors()),
262    };
263
264    let document_label = match struct_field_infos
265        .iter()
266        .filter(|f| f.label.is_present())
267        .collect::<Vec<_>>()
268        .as_slice()
269    {
270        [] => None,
271        [one] => Some(cleanup_ident(
272            one.ident
273                .as_ref()
274                .expect("this shouldn't be a tuple struct!!"),
275        )),
276        [head, tail @ ..] => {
277            return tail
278                .iter()
279                .fold(
280                    syn::Error::new(
281                        head.label.span(),
282                        "only one field can be defined as the label",
283                    ),
284                    |mut error, field| {
285                        error.combine(syn::Error::new(
286                            field.label.span(),
287                            "only one field can be defined as the label",
288                        ));
289                        error
290                    },
291                )
292                .into_compile_error()
293                .into()
294        }
295    }
296    .map_or(quote! { None }, |lit| quote! {Some(#lit)});
297
298    let document_sub_label = match struct_field_infos
299        .iter()
300        .filter(|f| f.sublabel.is_present())
301        .collect::<Vec<_>>()
302        .as_slice()
303    {
304        [] => None,
305        [one] => Some(cleanup_ident(
306            one.ident
307                .as_ref()
308                .expect("this shouldn't be a tuple struct!!"),
309        )),
310        [head, tail @ ..] => {
311            return tail
312                .iter()
313                .fold(
314                    syn::Error::new(
315                        head.sublabel.span(),
316                        "only one field can be defined as the sub label",
317                    ),
318                    |mut error, field| {
319                        error.combine(syn::Error::new(
320                            field.sublabel.span(),
321                            "only one field can be defined as the sub label",
322                        ));
323                        error
324                    },
325                )
326                .into_compile_error()
327                .into()
328        }
329    }
330    .map_or(quote! { None }, |lit| quote! {Some(#lit)});
331
332    let struct_validators = match struct_fields
333        .iter()
334        .map(ValidateInfo::from_field)
335        .collect::<Result<Vec<ValidateInfo>, darling::Error>>()
336    {
337        Ok(f) => f,
338        Err(e) => return TokenStream::from(e.write_errors()),
339    };
340
341    let fields = struct_field_infos
342        .iter()
343        .map(|f| field_to_info_call(f.to_owned()))
344        .collect::<Vec<_>>();
345
346    let validators = struct_validators
347        .iter()
348        .filter(|&f| !f.skip.is_present())
349        .map(|f| {
350            let ident = f.ident.as_ref().expect("this shouldn't be a tuple struct!");
351            let ident_str = ident.to_string();
352
353            if let Some(fn_ident) = f.with.as_ref() {
354                quote! {
355                    (#ident_str.into(), #fn_ident(&self.#ident, ctx.for_field(#ident_str)).await)
356                }
357            } else {
358                quote! {
359                    (#ident_str.into(), ::scalar_cms::validations::Validate::validate(&self.#ident, ctx.for_field(#ident_str)).await)
360                }
361            }
362        })
363        .collect::<Vec<_>>();
364
365    let validators_count = validators.len();
366
367    let output = quote! {
368        #[automatically_derived]
369        impl Document for #ident {
370            const IDENTIFIER: &'static str = #doc_identifier;
371            const TITLE: &'static str = #doc_title;
372            const LABEL: Option<&'static str> = #document_label;
373            const SUB_LABEL: Option<&'static str> = #document_sub_label;
374            const SINGLETON: bool = #singleton;
375
376            fn fields() -> &'static [::scalar_cms::EditorField] {
377                use ::scalar_cms::editor_field::ToEditorField;
378                static FIELDS: ::std::sync::LazyLock<Box<[EditorField]>> =
379                    ::std::sync::LazyLock::new(|| vec![
380                        #(#fields),*
381                    ].into_boxed_slice());
382
383                &FIELDS
384            }
385        }
386
387        #[automatically_derived]
388        impl ::scalar_cms::validations::Validate for #ident {
389            async fn validate<DB: ::scalar_cms::db::DatabaseConnection + ::scalar_cms::db::ContentActions<D> + Sync, D: ::scalar_cms::Document + Sync>(&self, ctx: ::scalar_cms::db::ValidationContext<'_, DB, D>) -> Result<(), ::scalar_cms::validations::ValidationError> {
390                let results: [(::scalar_cms::validations::Field, Result<(), ::scalar_cms::validations::ValidationError>); #validators_count] = [#(#validators),*];
391
392                let errors: Vec<::scalar_cms::validations::ErroredField> = results
393                    .into_iter()
394                    .filter_map(|(f, r)| r.err().map(|e| ::scalar_cms::validations::ErroredField { field: f, error: e}))
395                    .collect();
396
397                errors
398                    .is_empty()
399                    .then_some(())
400                    .ok_or(::scalar_cms::validations::ValidationError::Composite(errors))
401            }
402        }
403    };
404    output.into()
405}
406
407fn field_to_info_call(field: FieldInfo) -> proc_macro2::TokenStream {
408    let ty = field.ty;
409
410    let ident = field
411        .ident
412        .map(|i| cleanup_ident(&i))
413        .expect("this shouldn't be a tuple struct!!!!");
414    let title = field
415        .title
416        .unwrap_or(ident.to_case(convert_case::Case::Title));
417    let placeholder = if let Some(str) = field.placeholder {
418        quote! { Some(#str) }
419    } else {
420        quote! { None }
421    };
422    let component_key = if let Some(str) = field.editor_component {
423        quote! { Some(#str) }
424    } else {
425        quote! { None }
426    };
427
428    // let validator = match field.validate.is_present() {
429    //     true => quote! { Some(stringify!(#ty)) },
430    //     false => quote! { None },
431    // };
432
433    let default = match field.default {
434        Some(lit) => quote! { Some(#lit) },
435        None => {
436            quote! { None::<#ty> }
437        }
438    };
439    quote! {
440        <#ty as ::scalar_cms::editor_field::ToEditorField>::to_editor_field(#default, #ident, #title, #placeholder, None, #component_key)
441    }
442}
443
444/// cleans up idents which may start with r#, same as serde
445fn cleanup_ident(ident: &Ident) -> String {
446    ident.to_string().trim_start_matches("r#").to_string()
447}