cream_macros/
lib.rs

1//! cream-macros
2//!
3//! This crate defines procedural macros for the `cream`` crate.
4#![deny(missing_docs)]
5
6use std::{collections::HashMap, fs};
7
8use convert_case::{Case, Casing};
9use cream_core::{
10    Attribute, Mutability, ResourceType, Returned, Schema, SchemaExtension, Type, Uniqueness,
11};
12use proc_macro::TokenStream;
13use proc_macro2::TokenStream as TokenStream2;
14use quote::{format_ident, quote};
15use serde::de::DeserializeOwned;
16use syn::{
17    bracketed,
18    parse::{Parse, ParseStream},
19    parse_macro_input,
20    punctuated::Punctuated,
21    token::Bracket,
22    Ident, Token,
23};
24
25#[allow(unused)]
26struct DeclareResource {
27    path: String,
28    as_: Token![as],
29    name: Ident,
30    bracket_token: Bracket,
31    schemas: Punctuated<ReferencedSchema, Token![,]>,
32}
33
34struct ReferencedSchema {
35    path: String,
36}
37
38impl Parse for DeclareResource {
39    fn parse(input: ParseStream) -> syn::Result<Self> {
40        let content;
41        Ok(Self {
42            path: input.parse::<syn::LitStr>()?.value(),
43            as_: input.parse::<Token![as]>()?,
44            name: input.parse::<Ident>()?,
45            bracket_token: bracketed!(content in input),
46            schemas: Punctuated::parse_terminated(&content)?,
47        })
48    }
49}
50
51impl Parse for ReferencedSchema {
52    fn parse(input: ParseStream) -> syn::Result<Self> {
53        Ok(Self {
54            path: input.parse::<syn::LitStr>()?.value(),
55        })
56    }
57}
58
59fn load_static_resource<T: DeserializeOwned>(
60    path: &str,
61    referenced_files_hack: &mut Vec<TokenStream2>,
62) -> (T, String) {
63    let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or(".".into());
64    let path = std::path::Path::new(&root)
65        .join(path)
66        .canonicalize()
67        .unwrap()
68        .to_string_lossy()
69        .into_owned();
70    let content = fs::read_to_string(&path).expect("File not found");
71    referenced_files_hack.push(quote! {
72        const _: &str = include_str!(#path);
73    });
74    (
75        serde_json::from_str(&content).expect("Failed to parse JSON"),
76        content,
77    )
78}
79
80struct SchemaStruct {
81    declaration: TokenStream2,
82    ty: Ident,
83    create_ty: Ident,
84}
85
86const KEYWORDS: &[&str] = &["ref", "type"];
87
88fn sanitize_name(name: &str, casing: Case) -> Ident {
89    let converted = name.replace("$", "").to_case(casing);
90    if KEYWORDS.contains(&converted.as_str()) {
91        format_ident!("{}_", converted)
92    } else {
93        format_ident!("{}", converted)
94    }
95}
96
97fn declare_manager_trait(
98    manager: Ident,
99    ty: Ident,
100    create_ty: Ident,
101    resource_type_str: &str,
102    schemas: &HashMap<String, (Schema, String)>,
103) -> TokenStream2 {
104    let adapter = format_ident!("{}Adapter", manager);
105    let schema_arms = schemas.iter().map(|(schema_id, (_, schema_str))| {
106        quote! {
107            #schema_id => {
108                ::cream::hidden::serde_json::from_str(#schema_str).expect(concat!("Failed to deserialize ", #schema_id))
109            }
110        }
111    }).collect::<Vec<_>>();
112    quote! {
113        #[::cream::hidden::async_trait::async_trait]
114        pub trait #manager: ::std::fmt::Debug + Send + Sync + 'static {
115            async fn list(
116                &self,
117                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
118                args: ::cream::ListResourceArgs<'async_trait>,
119            ) -> ::std::result::Result<::cream::ListResourceResult<#ty>, ::cream::Error>;
120            async fn get(
121                &self,
122                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
123                args: ::cream::GetResourceArgs<'async_trait>
124            ) -> ::std::result::Result<#ty, ::cream::Error>;
125            async fn create(
126                &self,
127                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
128                resource: #create_ty
129            ) -> ::std::result::Result<String, ::cream::Error>;
130            async fn update(
131                &self,
132                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
133                args: ::cream::UpdateResourceArgs<'async_trait>
134            ) -> ::std::result::Result<(), ::cream::Error>;
135            async fn replace(
136                &self,
137                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
138                id: &'async_trait str, resource: #create_ty
139            ) -> Result<(), ::cream::Error>;
140            async fn delete(
141                &self,
142                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
143                id: &'async_trait str
144            ) -> ::std::result::Result<(), ::cream::Error>;
145
146            fn default_page_size(&self) -> usize {
147                50
148            }
149        }
150
151        #[derive(Debug)]
152        pub struct #adapter<T: #manager>(T);
153
154        #[::cream::hidden::async_trait::async_trait]
155        impl<T: #manager> ::cream::GenericResourceManager for #adapter<T> {
156            async fn list(
157                &self,
158                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
159                args: ::cream::ListResourceArgs<'async_trait>,
160            ) -> ::std::result::Result<::cream::ListResourceResult<::cream::hidden::ijson::IObject>, ::cream::Error> {
161                let result = self.0.list(parts, args).await?;
162                Ok(::cream::ListResourceResult {
163                    resources: result.resources.into_iter().map(|mut resource| {
164                        resource.locate();
165                        resource.to_object()
166                    }).collect(),
167                    total_count: result.total_count,
168                    items_per_page: result.items_per_page,
169                })
170            }
171
172            async fn get(
173                &self,
174                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
175                args: ::cream::GetResourceArgs<'async_trait>
176            ) -> ::std::result::Result<::cream::hidden::ijson::IObject, ::cream::Error> {
177                let mut resource = self.0.get(parts, args).await?;
178                resource.locate();
179                Ok(resource.to_object())
180            }
181
182            async fn create(
183                &self,
184                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
185                resource: ::cream::hidden::ijson::IObject
186            ) -> ::std::result::Result<String, ::cream::Error> {
187                let create_resource = #create_ty::from_object(&resource)?;
188                self.0.create(parts, create_resource).await
189            }
190
191            async fn update(
192                &self,
193                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
194                args: ::cream::UpdateResourceArgs<'async_trait>
195            ) -> ::std::result::Result<(), ::cream::Error> {
196                self.0.update(parts, args).await
197            }
198
199            async fn replace(
200                &self,
201                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
202                id: &str,
203                resource: ::cream::hidden::ijson::IObject
204            ) -> ::std::result::Result<(), ::cream::Error> {
205                let create_resource = #create_ty::from_object(&resource)?;
206                self.0.replace(parts, id, create_resource).await
207            }
208
209            async fn delete(
210                &self,
211                parts: &'async_trait ::cream::hidden::axum::http::request::Parts,
212                id: &str
213            ) -> ::std::result::Result<(), ::cream::Error> {
214                self.0.delete(parts, id).await
215            }
216            fn default_page_size(&self) -> usize {
217                self.0.default_page_size()
218            }
219
220            fn load_resource_type(&self) -> ::cream::ResourceType {
221                ::cream::hidden::serde_json::from_str(#resource_type_str).expect(concat!("Failed to deserialize resource type"))
222            }
223
224            fn load_schema(&self, id: &str) -> ::cream::Schema {
225                match id {
226                    #(#schema_arms)*
227                    _ => panic!("Unknown schema: {}", id),
228                }
229            }
230        }
231    }
232}
233
234#[allow(clippy::too_many_arguments)]
235fn declare_schema_struct(
236    struct_name: Ident,
237    attributes: &[Attribute],
238    schema_urn: TokenStream2,
239    parent_attr_name: Option<&str>,
240    manager: Option<Ident>,
241    extensions: &[SchemaExtension],
242    schemas: &HashMap<String, (Schema, String)>,
243    core_resource_type: Option<&ResourceType>,
244) -> SchemaStruct {
245    let mut fields = Vec::new();
246    let mut create_fields = Vec::new();
247    let mut other_declarations = Vec::new();
248    let mut field_consts = Vec::new();
249
250    let id_attribute = Attribute {
251        name: "id".into(),
252        type_: Type::String,
253        multi_valued: false,
254        description: "Unique identifier for the resource".into(),
255        required: false,
256        canonical_values: None,
257        case_exact: true,
258        mutability: Mutability::ReadOnly,
259        returned: Returned::Always,
260        uniqueness: Uniqueness::Server,
261        reference_types: None,
262        sub_attributes: None,
263    };
264
265    let external_id_attribute = Attribute {
266        name: "externalId".into(),
267        type_: Type::String,
268        multi_valued: false,
269        description: "External identifier for the resource".into(),
270        required: false,
271        canonical_values: None,
272        case_exact: true,
273        mutability: Mutability::ReadWrite,
274        returned: Returned::Default,
275        uniqueness: Uniqueness::None,
276        reference_types: None,
277        sub_attributes: None,
278    };
279
280    let extra_attributes = if core_resource_type.is_some() {
281        vec![id_attribute, external_id_attribute]
282    } else {
283        Vec::new()
284    };
285
286    for attr in extra_attributes.iter().chain(attributes) {
287        let name = sanitize_name(&attr.name, Case::Snake);
288        let upper_name = sanitize_name(&attr.name, Case::UpperSnake);
289        let pascal_name = sanitize_name(&attr.name, Case::Pascal);
290        let attr_name = &attr.name;
291
292        let (mut ty, mut create_ty) = match attr.type_ {
293            Type::String | Type::Binary => (quote! { String }, quote! { String }),
294            Type::Boolean => (quote! { bool }, quote! { bool }),
295            Type::Decimal => (quote! { f64 }, quote! { f64 }),
296            Type::Integer => (quote! { i64 }, quote! { i64 }),
297            Type::DateTime => (quote! { ::cream::DateTime }, quote! { ::cream::DateTime }),
298            Type::Reference => (quote! { ::cream::Reference }, quote! { ::cream::Reference }),
299            Type::Complex => {
300                let singular_name = if attr.multi_valued {
301                    if let Some(prefix) = pascal_name.to_string().strip_suffix("ses") {
302                        format_ident!("{}s", prefix)
303                    } else if let Some(prefix) = pascal_name.to_string().strip_suffix("s") {
304                        format_ident!("{}", prefix)
305                    } else {
306                        pascal_name
307                    }
308                } else {
309                    pascal_name
310                };
311                let SchemaStruct {
312                    declaration,
313                    ty,
314                    create_ty,
315                } = declare_schema_struct(
316                    format_ident!("{}{}", struct_name, singular_name),
317                    attr.sub_attributes
318                        .as_ref()
319                        .expect("Complex attribute must have sub-attributes"),
320                    schema_urn.clone(),
321                    Some(attr_name),
322                    None,
323                    &[],
324                    schemas,
325                    None,
326                );
327                other_declarations.push(declaration);
328                (quote! { #ty }, quote! { #create_ty })
329            }
330        };
331
332        // Define constants for referencing fields
333        if let Some(parent_attr_name) = parent_attr_name {
334            field_consts.push(quote! {
335                pub const #upper_name: ::cream::AttrPathRef<'static> = ::cream::AttrPathRef {
336                    urn: #schema_urn,
337                    name: #parent_attr_name,
338                    sub_attr: Some(#attr_name),
339                };
340            });
341        } else {
342            field_consts.push(quote! {
343                pub const #upper_name: ::cream::AttrPathRef<'static> = ::cream::AttrPathRef {
344                    urn: #schema_urn,
345                    name: #attr_name,
346                    sub_attr: None,
347                };
348            });
349        }
350
351        // "read" type
352        let is_present = !matches!(attr.returned, Returned::Never)
353            && !matches!(attr.mutability, Mutability::WriteOnly);
354
355        if is_present {
356            let is_optional = matches!(attr.returned, Returned::Default | Returned::Request);
357            let mut serde_attrs = Vec::new();
358            serde_attrs.push(quote! { rename = #attr_name });
359
360            if attr.multi_valued {
361                ty = quote! { Vec<#ty> };
362            }
363
364            if is_optional {
365                serde_attrs.push(quote! { skip_serializing_if = "Option::is_none" });
366                ty = quote! { Option<#ty> };
367            }
368
369            fields.push(quote! {
370                #[serde( #(#serde_attrs),* )]
371                pub #name: #ty,
372            });
373        }
374
375        // "create" type
376        let create_is_present = !matches!(attr.mutability, Mutability::ReadOnly);
377
378        if create_is_present {
379            let create_is_optional = !attr.required;
380            let mut serde_attrs = Vec::new();
381            serde_attrs.push(quote! { rename = #attr_name });
382
383            if attr.multi_valued {
384                create_ty = quote! { Vec<#create_ty> };
385            }
386
387            if create_is_optional {
388                if attr.multi_valued {
389                    serde_attrs.push(quote! { default });
390                } else {
391                    create_ty = quote! { Option<#create_ty> };
392                }
393            }
394
395            create_fields.push(quote! {
396                #[serde( #(#serde_attrs),* )]
397                pub #name: #create_ty,
398            });
399        }
400    }
401
402    for (i, ext) in extensions.iter().enumerate() {
403        let name = format_ident!("ext{}", i);
404        let schema_id = &ext.schema;
405        let SchemaStruct {
406            declaration,
407            ty,
408            create_ty,
409        } = declare_schema_struct(
410            format_ident!("{}Ext{}", struct_name, i),
411            &schemas[&ext.schema].0.attributes,
412            quote! {Some(#schema_id)},
413            None,
414            None,
415            &[],
416            schemas,
417            None,
418        );
419        let ty = quote! { #ty };
420        let mut create_ty = quote! { #create_ty };
421
422        other_declarations.push(declaration);
423        let mut serde_attrs = Vec::new();
424        serde_attrs.push(quote! { rename = #schema_id });
425
426        fields.push(quote! {
427            #[serde( #(#serde_attrs),* )]
428            pub #name: #ty,
429        });
430
431        let mut serde_attrs = Vec::new();
432        serde_attrs.push(quote! { rename = #schema_id });
433        if !ext.required {
434            create_ty = quote! { Option<#create_ty> };
435            serde_attrs.push(quote! { default });
436        }
437
438        create_fields.push(quote! {
439            #[serde( #(#serde_attrs),* )]
440            pub #name: #create_ty,
441        });
442    }
443
444    let create_struct_name = format_ident!("Create{}", struct_name);
445
446    let mut other_methods = Vec::new();
447    if let Some(manager) = manager {
448        let adapter = format_ident!("{}Adapter", manager);
449        other_methods.push(quote! {
450            pub fn manage(manager: impl #manager) -> impl ::cream::GenericResourceManager {
451                #adapter(manager)
452            }
453        });
454    };
455
456    if let Some(resource_type) = core_resource_type {
457        let resource_name = &resource_type.name;
458        let endpoint = &resource_type.endpoint;
459        let mut schema_type_names = Vec::new();
460
461        let schema_id = &resource_type.schema;
462        let schema_type_name = format_ident!("{}Schema", struct_name);
463        let resource_type_name = format_ident!("{}ResourceType", struct_name);
464        other_declarations.push(quote! {
465            ::cream::declare_schema!(#schema_type_name = #schema_id);
466            ::cream::declare_resource_type!(#resource_type_name = #resource_name);
467        });
468        schema_type_names.push(schema_type_name);
469
470        for (i, ext) in extensions.iter().enumerate() {
471            let schema_id = &ext.schema;
472            let schema_type_name = format_ident!("{}Ext{}Schema", struct_name, i);
473            other_declarations.push(quote! {
474                ::cream::declare_schema!(#schema_type_name = #schema_id);
475            });
476            schema_type_names.push(schema_type_name);
477        }
478
479        other_methods.push(quote! {
480            pub fn locate(&mut self) {
481                self.meta.location = Some(::cream::Reference::new_relative(&format!(
482                    "{}/{}",
483                    #endpoint,
484                    self.id
485                )));
486            }
487        });
488
489        if schema_type_names.len() == 1 {
490            // If there's one schema, we can't use a tuple, because serde will not serialize it as an array
491            // if it has a single element, so use a fixed size array instead.
492            let schema_type_name = &schema_type_names[0];
493            fields.push(quote! {
494                pub schemas: [#schema_type_name; 1],
495            });
496        } else {
497            // With more than once schema we must use a tuple since the schemas are distinct types.
498            fields.push(quote! {
499                pub schemas: (#(#schema_type_names),*),
500            });
501        }
502
503        fields.push(quote! {
504            pub meta: ::cream::Meta<#resource_type_name>,
505        })
506    }
507
508    let declaration = quote! {
509        #(#other_declarations)*
510
511        #[derive(Debug, ::cream::hidden::serde::Serialize, Clone)]
512        pub struct #struct_name {
513            #(
514                #fields
515            )*
516        }
517
518        impl #struct_name {
519            #(
520                #field_consts
521            )*
522
523            #(
524                #other_methods
525            )*
526
527            pub fn to_object(&self) -> ::cream::hidden::ijson::IObject {
528                ::cream::hidden::ijson::to_value(self)
529                    .expect("Infallible serialization")
530                    .into_object()
531                    .expect("Resources must serialize as objects")
532            }
533        }
534
535        #[derive(Debug, ::cream::hidden::serde::Deserialize, Clone)]
536        pub struct #create_struct_name {
537            #(
538                #create_fields
539            )*
540        }
541
542        impl #create_struct_name {
543            pub fn from_object(object: &::cream::hidden::ijson::IObject) -> ::std::result::Result<Self, ::cream::Error> {
544                ::cream::hidden::ijson::from_value(object.as_ref()).map_err(|e| ::cream::Error::new(
545                    ::cream::hidden::axum::http::StatusCode::BAD_REQUEST,
546                    Some(::cream::ErrorType::InvalidValue),
547                    e.to_string(),
548                ))
549            }
550        }
551    };
552    SchemaStruct {
553        ty: struct_name,
554        create_ty: create_struct_name,
555        declaration,
556    }
557}
558
559/// Generate support code for a resource type.
560///
561/// Syntax:
562/// ```ignore
563/// declare_resource!("<path/to/resource_type.json>" as <ResourceName> [
564///     "<path/to/core_schema.json>",
565///     ...<optional extension schemas>,
566/// ]);
567/// ```
568#[proc_macro]
569pub fn declare_resource(input: TokenStream) -> TokenStream {
570    let DeclareResource {
571        path,
572        name,
573        schemas: ref_schemas,
574        ..
575    } = parse_macro_input!(input as DeclareResource);
576
577    // Load the resource type and referenced schemas
578    let mut referenced_files_hack = Vec::new();
579    let (resource_type, _resource_type_str) =
580        load_static_resource::<ResourceType>(&path, &mut referenced_files_hack);
581    let mut schemas = HashMap::new();
582    for ref_schema in ref_schemas {
583        let (schema, schema_str) =
584            load_static_resource::<Schema>(&ref_schema.path, &mut referenced_files_hack);
585        schemas.insert(schema.id.clone(), (schema, schema_str));
586    }
587
588    let manager = format_ident!("{}Manager", name);
589
590    let SchemaStruct {
591        ty,
592        create_ty,
593        declaration,
594    } = declare_schema_struct(
595        name.clone(),
596        &schemas[&resource_type.schema].0.attributes,
597        quote! { None },
598        None,
599        Some(manager.clone()),
600        &resource_type.schema_extensions,
601        &schemas,
602        Some(&resource_type),
603    );
604
605    let mut result = TokenStream2::new();
606    result.extend(referenced_files_hack);
607    result.extend(declaration);
608    result.extend(declare_manager_trait(
609        manager,
610        ty,
611        create_ty,
612        &serde_json::to_string(&resource_type).unwrap(),
613        &schemas,
614    ));
615    result.into()
616}