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#[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#[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#[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 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
444fn cleanup_ident(ident: &Ident) -> String {
446 ident.to_string().trim_start_matches("r#").to_string()
447}