1use core::iter::Iterator;
2use std::collections::{HashMap, HashSet};
3
4use cinderblock_extension_api::{
5 Accept, OrderDirection, ReadFilterValue, RelationDecl, RelationKind, ResourceActionInputKind,
6 ResourceAttributeInput, ResourceMacroInput, UpdateChange,
7};
8use syn::{Ident, Type, spanned::Spanned};
9
10fn is_option_type(ty: &Type) -> bool {
16 if let Type::Path(type_path) = ty {
17 type_path
18 .path
19 .segments
20 .last()
21 .is_some_and(|seg| seg.ident == "Option")
22 } else {
23 false
24 }
25}
26
27#[proc_macro]
28pub fn resource(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
29 let raw_tokens: proc_macro2::TokenStream = item.clone().into();
32
33 let input = syn::parse_macro_input!(item as ResourceMacroInput);
34
35 let ident = input.name.last().expect("Missing name segment");
36
37 let fields = input
38 .attributes
39 .iter()
40 .map(ResourceAttributeInput::to_field_definition);
41
42 let primary_key_type = {
43 let fields = input
44 .attributes
45 .iter()
46 .filter(|attr| attr.primary_key.value())
47 .collect::<Vec<_>>();
48
49 match fields.len() {
50 0 => todo!("Support no primary keys"),
51 1 => {
52 let ty = fields[0].ty.clone();
53 quote::quote! { #ty }
54 }
55 _ => todo!("Support multiple primary keys"),
56 }
57 };
58
59 let primary_key_generated = {
60 let fields = input
61 .attributes
62 .iter()
63 .filter(|attr| attr.primary_key.value())
64 .collect::<Vec<_>>();
65
66 match fields.len() {
67 0 => todo!("Support no primary keys"),
68 1 => fields[0].generated.value(),
69 _ => todo!("Support multiple primary keys"),
70 }
71 };
72
73 let primary_key_value = {
74 let fields = input
75 .attributes
76 .iter()
77 .filter(|attr| attr.primary_key.value())
78 .collect::<Vec<_>>();
79
80 match fields.len() {
81 0 => todo!("Support no primary keys"),
82 1 => {
83 let ty = fields[0].name.clone();
84 quote::quote! { &self.#ty }
85 }
86 _ => todo!("Support multiple primary keys"),
87 }
88 };
89
90 let data_layer_specified = input.data_layer.is_some();
91
92 let relations = &input.relations;
93
94 let actions = input.actions.iter().map(|action| match &action.kind {
95 ResourceActionInputKind::Read(read_action) => {
96 let action_name = convert_case::ccase!(pascal, action.name.to_string());
97 let action_name = Ident::new(&action_name, action.name.span());
98
99 let is_get = read_action.get;
100 let is_paged = read_action.paged.is_some();
101 let has_loads = !read_action.load.is_empty();
102
103 if is_get && !has_loads {
110 return quote::quote! {
111 struct #action_name;
112
113 impl cinderblock_core::ReadAction for #action_name {
114 type Output = #ident;
115 type Arguments = <#ident as cinderblock_core::Resource>::PrimaryKey;
116 type Response = #ident;
117 }
118 };
119 }
120
121 if is_get && has_loads {
128 let loaded_relations: Vec<&RelationDecl> = read_action
129 .load
130 .iter()
131 .map(|name| {
132 relations
133 .iter()
134 .find(|r| r.name == *name)
135 .expect("load reference validated during parsing")
136 })
137 .collect();
138
139 let wrapper_name =
140 Ident::new(&format!("{action_name}Response"), action.name.span());
141
142 let relation_fields = loaded_relations.iter().map(|rel| {
143 let rel_name = &rel.name;
144 let rel_ty = &rel.ty;
145 match rel.kind {
146 RelationKind::BelongsTo => quote::quote! {
147 pub #rel_name: #rel_ty
148 },
149 RelationKind::HasMany => quote::quote! {
150 pub #rel_name: Vec<#rel_ty>
151 },
152 }
153 });
154
155 let response_wrapper = quote::quote! {
156 #[derive(::std::fmt::Debug, ::std::clone::Clone, cinderblock_core::serde::Serialize)]
157 pub struct #wrapper_name {
158 #[serde(flatten)]
159 pub base: #ident,
160 #(#relation_fields),*
161 }
162 };
163
164 let relation_loads = loaded_relations.iter().map(|rel| {
165 let rel_ty = &rel.ty;
166 let source_attr = &rel.source_attribute;
167 let map_name = Ident::new(
168 &format!("{}_map", rel.name),
169 rel.name.span(),
170 );
171
172 match rel.kind {
173 RelationKind::BelongsTo => {
174 quote::quote! {
175 let all_related: Vec<#rel_ty> = dl.load_all::<#rel_ty>().await;
176 let #map_name: ::std::collections::HashMap<String, #rel_ty> = all_related
177 .into_iter()
178 .map(|r| {
179 use cinderblock_core::Resource;
180 (r.primary_key().to_string(), r)
181 })
182 .collect();
183 }
184 }
185 RelationKind::HasMany => {
186 quote::quote! {
187 let all_related: Vec<#rel_ty> = dl.load_all::<#rel_ty>().await;
188 let mut #map_name: ::std::collections::HashMap<String, Vec<#rel_ty>> =
189 ::std::collections::HashMap::new();
190 for r in all_related {
191 let key = r.#source_attr.to_string();
192 #map_name.entry(key).or_default().push(r);
193 }
194 }
195 }
196 }
197 });
198
199 let relation_field_inits = loaded_relations.iter().map(|rel| {
200 let rel_name = &rel.name;
201 let source_attr = &rel.source_attribute;
202 let map_name = Ident::new(
203 &format!("{}_map", rel.name),
204 rel.name.span(),
205 );
206
207 match rel.kind {
208 RelationKind::BelongsTo => {
209 let rel_ty = &rel.ty;
210 let rel_name_str = rel.name.to_string();
211 quote::quote! {
212 #rel_name: #map_name
213 .get(&base.#source_attr.to_string())
214 .cloned()
215 .ok_or_else(|| {
216 cinderblock_core::ReadError::DataLayer(
217 format!(
218 "belongs_to relation `{}` of type `{}`: no record found for FK value `{}`",
219 #rel_name_str,
220 ::std::any::type_name::<#rel_ty>(),
221 base.#source_attr,
222 ).into(),
223 )
224 })?
225 }
226 }
227 RelationKind::HasMany => {
228 quote::quote! {
229 #rel_name: {
230 use cinderblock_core::Resource;
231 #map_name
232 .get(&base.primary_key().to_string())
233 .cloned()
234 .unwrap_or_default()
235 }
236 }
237 }
238 }
239 });
240
241 let data_layer_block = if data_layer_specified {
242 quote::quote! { }
243 } else {
244 quote::quote! {
245 impl cinderblock_core::PerformReadOne<#action_name> for cinderblock_core::data_layer::in_memory::InMemoryDataLayer {
246 async fn read_one(&self, args: &<#action_name as cinderblock_core::ReadAction>::Arguments) -> Result<<#action_name as cinderblock_core::ReadAction>::Response, cinderblock_core::ReadError> {
247 let dl = self;
248
249 let base = <Self as cinderblock_core::data_layer::DataLayer<#ident>>::read(dl, args).await?;
250
251 #(#relation_loads)*
252
253 Ok(#wrapper_name {
254 #(#relation_field_inits,)*
255 base,
256 })
257 }
258 }
259 }
260 };
261
262 return quote::quote! {
263 #response_wrapper
264
265 pub struct #action_name;
266
267 impl cinderblock_core::ReadAction for #action_name {
268 type Output = #ident;
269 type Arguments = <#ident as cinderblock_core::Resource>::PrimaryKey;
270 type Response = #wrapper_name;
271 }
272
273 #data_layer_block
274 };
275 }
276
277 let has_user_arguments = !read_action.arguments.is_empty();
286 let needs_arguments_struct = has_user_arguments || is_paged;
287
288 let (arguments_type, arguments_struct) = if needs_arguments_struct {
289 let args_name = Ident::new(&format!("{action_name}Arguments"), action.name.span());
290 let user_arg_fields = read_action.arguments.iter().map(|arg| {
291 let name = &arg.name;
292 let ty = &arg.ty;
293 quote::quote! { pub #name: #ty }
294 });
295
296 let paged_fields = if is_paged {
298 quote::quote! {
299 pub page: Option<u32>,
300 pub per_page: Option<u32>,
301 }
302 } else {
303 quote::quote! {}
304 };
305
306 (
307 quote::quote! { #args_name },
308 quote::quote! {
309 #[derive(::std::fmt::Debug, cinderblock_core::serde::Deserialize)]
310 pub struct #args_name {
311 #(#user_arg_fields,)*
312 #paged_fields
313 }
314 },
315 )
316 } else {
317 (quote::quote! { () }, quote::quote! {})
318 };
319
320 let paged_impl = if let Some(paged_config) = &read_action.paged {
326 let args_name = Ident::new(&format!("{action_name}Arguments"), action.name.span());
327
328 let default_per_page = match paged_config.default_per_page {
329 Some(n) => quote::quote! { #n },
330 None => quote::quote! { cinderblock_core::DEFAULT_PER_PAGE },
331 };
332
333 let per_page_body = if let Some(max) = paged_config.max_per_page {
336 quote::quote! {
337 self.per_page.unwrap_or(#default_per_page).min(#max)
338 }
339 } else {
340 quote::quote! {
341 self.per_page.unwrap_or(#default_per_page)
342 }
343 };
344
345 quote::quote! {
346 impl cinderblock_core::Paged for #args_name {
347 fn page(&self) -> u32 {
348 self.page.unwrap_or(1)
349 }
350
351 fn per_page(&self) -> u32 {
352 #per_page_body
353 }
354 }
355 }
356 } else {
357 quote::quote! {}
358 };
359
360 let loaded_relations: Vec<&RelationDecl> = read_action
370 .load
371 .iter()
372 .map(|name| {
373 relations
374 .iter()
375 .find(|r| r.name == *name)
376 .expect("load reference validated during parsing")
377 })
378 .collect();
379
380 let response_wrapper = if has_loads {
381 let wrapper_name =
382 Ident::new(&format!("{action_name}Response"), action.name.span());
383
384 let relation_fields = loaded_relations.iter().map(|rel| {
385 let rel_name = &rel.name;
386 let rel_ty = &rel.ty;
387 match rel.kind {
388 RelationKind::BelongsTo => quote::quote! {
389 pub #rel_name: #rel_ty
390 },
391 RelationKind::HasMany => quote::quote! {
392 pub #rel_name: Vec<#rel_ty>
393 },
394 }
395 });
396
397 quote::quote! {
398 #[derive(::std::fmt::Debug, ::std::clone::Clone, cinderblock_core::serde::Serialize)]
399 pub struct #wrapper_name {
400 #[serde(flatten)]
401 pub base: #ident,
402 #(#relation_fields),*
403 }
404 }
405 } else {
406 quote::quote! {}
407 };
408
409 let response_type = if has_loads {
417 let wrapper_name =
418 Ident::new(&format!("{action_name}Response"), action.name.span());
419 quote::quote! { Vec<#wrapper_name> }
420 } else if is_paged {
421 quote::quote! { cinderblock_core::PaginatedResult<#ident> }
422 } else {
423 quote::quote! { Vec<#ident> }
424 };
425
426 let build_filters = |read_action: &cinderblock_extension_api::ActionRead| {
431 read_action.filters.iter().map(|filter| {
432 let field = &filter.field;
433 let op = match filter.op {
434 cinderblock_extension_api::ReadFilterOperation::Eq => quote::quote! { == },
435 };
436 match &filter.value {
437 ReadFilterValue::Literal(expr) => {
438 quote::quote! {
439 row.#field #op #expr &&
440 }
441 }
442 ReadFilterValue::Arg(arg_name) => {
443 let arg_decl = read_action
444 .arguments
445 .iter()
446 .find(|a| a.name == *arg_name)
447 .expect("arg reference validated during parsing");
448 if is_option_type(&arg_decl.ty) {
449 quote::quote! {
450 args.#arg_name.as_ref().map_or(true, |v| row.#field #op *v) &&
451 }
452 } else {
453 quote::quote! {
454 row.#field #op args.#arg_name &&
455 }
456 }
457 }
458 }
459 }).collect::<Vec<_>>()
460 };
461
462 let build_order_sort = |read_action: &cinderblock_extension_api::ActionRead| -> Option<proc_macro2::TokenStream> {
471 if read_action.orders.is_empty() {
472 return None;
473 }
474
475 let mut clauses = read_action.orders.iter();
476 let first = clauses.next().unwrap();
477 let first_field = &first.field;
478 let first_cmp = match first.direction {
479 OrderDirection::Asc => quote::quote! { a.#first_field.cmp(&b.#first_field) },
480 OrderDirection::Desc => quote::quote! { b.#first_field.cmp(&a.#first_field) },
481 };
482
483 let rest: Vec<_> = clauses.map(|clause| {
484 let field = &clause.field;
485 match clause.direction {
486 OrderDirection::Asc => quote::quote! { .then_with(|| a.#field.cmp(&b.#field)) },
487 OrderDirection::Desc => quote::quote! { .then_with(|| b.#field.cmp(&a.#field)) },
488 }
489 }).collect();
490
491 Some(quote::quote! { #first_cmp #(#rest)* })
492 };
493
494 let data_layer_block = if data_layer_specified {
503 quote::quote! { }
504 } else if has_loads {
505 let filters = build_filters(read_action);
514 let order_sort = build_order_sort(read_action);
515 let order_sort_block = order_sort.as_ref().map(|cmp_body| {
516 quote::quote! { base_rows.sort_by(|a, b| #cmp_body); }
517 }).unwrap_or_default();
518
519 let relation_loads = loaded_relations.iter().map(|rel| {
529 let rel_ty = &rel.ty;
530 let source_attr = &rel.source_attribute;
531 let map_name = Ident::new(
532 &format!("{}_map", rel.name),
533 rel.name.span(),
534 );
535
536 match rel.kind {
537 RelationKind::BelongsTo => {
538 quote::quote! {
542 let all_related: Vec<#rel_ty> = dl.load_all::<#rel_ty>().await;
543 let #map_name: ::std::collections::HashMap<String, #rel_ty> = all_related
544 .into_iter()
545 .map(|r| {
546 use cinderblock_core::Resource;
547 (r.primary_key().to_string(), r)
548 })
549 .collect();
550 }
551 }
552 RelationKind::HasMany => {
553 quote::quote! {
558 let all_related: Vec<#rel_ty> = dl.load_all::<#rel_ty>().await;
559 let mut #map_name: ::std::collections::HashMap<String, Vec<#rel_ty>> =
560 ::std::collections::HashMap::new();
561 for r in all_related {
562 let key = r.#source_attr.to_string();
563 #map_name.entry(key).or_default().push(r);
564 }
565 }
566 }
567 }
568 });
569
570 let wrapper_name =
575 Ident::new(&format!("{action_name}Response"), action.name.span());
576
577 let relation_field_inits = loaded_relations.iter().map(|rel| {
578 let rel_name = &rel.name;
579 let source_attr = &rel.source_attribute;
580 let map_name = Ident::new(
581 &format!("{}_map", rel.name),
582 rel.name.span(),
583 );
584
585 match rel.kind {
586 RelationKind::BelongsTo => {
587 let rel_ty = &rel.ty;
588 let rel_name_str = rel.name.to_string();
589 quote::quote! {
590 #rel_name: #map_name
591 .get(&row.#source_attr.to_string())
592 .cloned()
593 .ok_or_else(|| {
594 cinderblock_core::ListError::DataLayer(
595 format!(
596 "belongs_to relation `{}` of type `{}`: no record found for FK value `{}`",
597 #rel_name_str,
598 ::std::any::type_name::<#rel_ty>(),
599 row.#source_attr,
600 ).into(),
601 )
602 })?
603 }
604 }
605 RelationKind::HasMany => {
606 quote::quote! {
607 #rel_name: {
608 use cinderblock_core::Resource;
609 #map_name
610 .get(&row.primary_key().to_string())
611 .cloned()
612 .unwrap_or_default()
613 }
614 }
615 }
616 }
617 });
618
619 quote::quote! {
620 impl cinderblock_core::PerformRead<#action_name> for cinderblock_core::data_layer::in_memory::InMemoryDataLayer {
621 async fn read(&self, args: &<#action_name as cinderblock_core::ReadAction>::Arguments) -> Result<<#action_name as cinderblock_core::ReadAction>::Response, cinderblock_core::ListError> {
622 let dl = self;
623
624 let mut base_rows: Vec<#ident> = dl.load_all::<#ident>().await
626 .into_iter()
627 .filter(|row| { #(#filters)* true })
628 .collect();
629
630 #order_sort_block
632
633 #(#relation_loads)*
635
636 let results: Result<Vec<#wrapper_name>, cinderblock_core::ListError> = base_rows
638 .into_iter()
639 .map(|row| -> Result<#wrapper_name, cinderblock_core::ListError> {
640 Ok(#wrapper_name {
641 #(#relation_field_inits,)*
642 base: row,
643 })
644 })
645 .collect();
646
647 results
648 }
649 }
650 }
651 } else if is_paged {
652 let filters = build_filters(read_action);
653 let order_sort = build_order_sort(read_action);
654 let order_sort_block = order_sort.as_ref().map(|cmp_body| {
655 quote::quote! { filtered.sort_by(|a, b| #cmp_body); }
656 }).unwrap_or_default();
657
658 quote::quote! {
659 impl cinderblock_core::data_layer::in_memory::InMemoryPagedReadAction for #action_name {
660 fn filter(row: &Self::Output, args: &Self::Arguments) -> bool {
661 #(#filters)* true
662 }
663 }
664
665 impl cinderblock_core::data_layer::in_memory::InMemoryPerformRead for #action_name {
666 fn execute(
667 all: impl Iterator<Item = Self::Output>,
668 args: &Self::Arguments,
669 ) -> Self::Response {
670 use cinderblock_core::Paged;
671
672 let mut filtered: Vec<Self::Output> = all
673 .filter(|row| <Self as cinderblock_core::data_layer::in_memory::InMemoryPagedReadAction>::filter(row, args))
674 .collect();
675
676 #order_sort_block
677
678 let total = filtered.len() as u64;
679 let page = args.page();
680 let per_page = args.per_page();
681 let total_pages = if total == 0 { 1 } else { ((total as u32).saturating_add(per_page - 1)) / per_page };
682
683 let skip = ((page.saturating_sub(1)) as usize) * (per_page as usize);
684 let data: Vec<Self::Output> = filtered
685 .into_iter()
686 .skip(skip)
687 .take(per_page as usize)
688 .collect();
689
690 cinderblock_core::PaginatedResult {
691 data,
692 meta: cinderblock_core::PaginationMeta {
693 page,
694 per_page,
695 total,
696 total_pages,
697 },
698 }
699 }
700 }
701 }
702 } else {
703 let filters = build_filters(read_action);
714 let order_sort = build_order_sort(read_action);
715
716 let execute_body = if let Some(cmp_body) = order_sort {
717 quote::quote! {
718 let mut results: Vec<Self::Output> = all
719 .filter(|row| <Self as cinderblock_core::data_layer::in_memory::InMemoryReadAction>::filter(row, args))
720 .collect();
721 results.sort_by(|a, b| #cmp_body);
722 results
723 }
724 } else {
725 quote::quote! {
726 all.filter(|row| <Self as cinderblock_core::data_layer::in_memory::InMemoryReadAction>::filter(row, args))
727 .collect()
728 }
729 };
730
731 quote::quote! {
732 impl cinderblock_core::data_layer::in_memory::InMemoryReadAction for #action_name {
733 fn filter(row: &Self::Output, args: &Self::Arguments) -> bool {
734 #(#filters)* true
735 }
736 }
737
738 impl cinderblock_core::data_layer::in_memory::InMemoryPerformRead for #action_name {
739 fn execute(
740 all: impl Iterator<Item = Self::Output>,
741 args: &Self::Arguments,
742 ) -> Self::Response {
743 #execute_body
744 }
745 }
746 }
747 };
748
749 quote::quote! {
750 #arguments_struct
751
752 #paged_impl
753
754 #response_wrapper
755
756 pub struct #action_name;
757
758 impl cinderblock_core::ReadAction for #action_name {
759 type Output = #ident;
760
761 type Arguments = #arguments_type;
762
763 type Response = #response_type;
764 }
765
766 #data_layer_block
767 }
768 }
769 ResourceActionInputKind::Create { accept } => {
770 let action_name = convert_case::ccase!(pascal, action.name.to_string());
771 let action_name = Ident::new(&action_name, action.name.span());
772 let input_name = Ident::new(&format!("{action_name}Input"), action.name.span());
773
774 let attributes = input.attributes.iter().filter(|attr| attr.writable.value());
775
776 let (present, mut missing_names) = match accept {
777 Accept::Default => (
778 attributes
779 .map(|attr| (attr.name.to_string(), attr))
780 .collect::<HashMap<_, _>>(),
781 HashMap::new(),
782 ),
783 Accept::Only(idents) => {
784 let idents = idents
785 .iter()
786 .map(|ident| ident.to_string())
787 .collect::<HashSet<_>>();
788
789 attributes.fold(
790 (HashMap::new(), HashMap::new()),
791 |(mut present, mut missing), attr| {
792 if idents.contains(&attr.name.to_string()) {
793 present.insert(attr.name.to_string(), attr);
794 } else {
795 missing.insert(attr.name.to_string(), attr);
796 }
797 (present, missing)
798 },
799 )
800 }
801 };
802
803 input
804 .attributes
805 .iter()
806 .filter(|attr| {
807 !attr.writable.value() || !present.contains_key(&attr.name.to_string())
808 })
809 .for_each(|attr| {
810 missing_names.insert(attr.name.to_string(), attr);
811 });
812
813 let attributes = present.values().map(|attr| attr.to_field_definition());
814
815 let missing_names = missing_names.values().map(|attr| attr.to_default());
816
817 let present_names = present.values().map(|attr| attr.name.clone());
818
819 quote::quote! {
820 #[derive(::std::fmt::Debug)]
821 pub struct #action_name;
822
823 #[derive(::std::fmt::Debug, cinderblock_core::serde::Deserialize)]
824 pub struct #input_name {
825 #(pub #attributes),*
826 }
827
828 impl cinderblock_core::Create<#action_name> for #ident {
829 type Input = #input_name;
830
831 fn from_create_input(input: Self::Input) -> Self {
832 #ident {
833 #(#present_names: input.#present_names,)*
835
836 #(#missing_names),*
838 }
839 }
840 }
841 }
842 }
843 ResourceActionInputKind::Update(update) => {
844 let action_name = convert_case::ccase!(pascal, action.name.to_string());
845 let action_name = Ident::new(&action_name, action.name.span());
846 let input_name = Ident::new(&format!("{action_name}Input"), action.name.span());
847
848 let attributes = input.attributes.iter().filter(|attr| attr.writable.value());
849
850 let present = match &update.accept {
851 Accept::Default => attributes.collect::<Vec<_>>(),
852 Accept::Only(idents) => {
853 let idents = idents
854 .iter()
855 .map(|ident| ident.to_string())
856 .collect::<HashSet<_>>();
857
858 attributes
859 .filter(|attr| idents.contains(&attr.name.to_string()))
860 .collect()
861 }
862 };
863
864 let field_definitions = present.iter().map(|attr| attr.to_field_definition());
865
866 let field_assignments = present.iter().map(|attr| {
871 let name = &attr.name;
872 quote::quote! { self.#name = input.#name; }
873 });
874
875 let change_ref_calls =
882 update
883 .changes
884 .iter()
885 .enumerate()
886 .filter_map(|(i, change)| match change {
887 UpdateChange::ChangeRef(closure) => {
888 let param = closure
889 .inputs
890 .first()
891 .expect("change_ref closure must have exactly one parameter");
892 let body = &closure.body;
893 let binding = Ident::new(&format!("change_ref_{i}"), param.span());
894 Some(quote::quote! {
895 let #binding = |#param: &mut Self| #body;
896 #binding(self);
897 })
898 }
899 UpdateChange::Change(_) => None,
901 });
902
903 quote::quote! {
904 #[derive(::std::fmt::Debug)]
905 pub struct #action_name;
906
907 #[derive(::std::fmt::Debug, cinderblock_core::serde::Deserialize)]
908 pub struct #input_name {
909 #(pub #field_definitions),*
910 }
911
912 impl cinderblock_core::Update<#action_name> for #ident {
913 type Input = #input_name;
914
915 fn apply_update_input(&mut self, input: Self::Input) {
916 #(#field_assignments)*
917 #(#change_ref_calls)*
918 }
919 }
920 }
921 }
922 ResourceActionInputKind::Destroy => {
923 let action_name = convert_case::ccase!(pascal, action.name.to_string());
929 let action_name = Ident::new(&action_name, action.name.span());
930
931 quote::quote! {
932 #[derive(::std::fmt::Debug)]
933 pub struct #action_name;
934
935 impl cinderblock_core::Destroy<#action_name> for #ident {}
936 }
937 }
938 });
939
940 let name_segments: Vec<String> = input
941 .name
942 .iter()
943 .map(|segment| segment.to_string())
944 .collect();
945 let resource_name_literal = name_segments.join(".");
946
947 let data_layer_path = input.data_layer.map_or_else(
952 || quote::quote! { cinderblock_core::data_layer::in_memory::InMemoryDataLayer },
953 |path| quote::quote! { #path },
954 );
955
956 let extension_calls = input.extensions.iter().map(|ext| {
964 let path = &ext.path;
965 let config_tokens = &ext.config_tokens;
966
967 quote::quote! {
968 #path::__resource_extension! {
969 { #raw_tokens }
970
971 config = {
972 #config_tokens
973 }
974 }
975 }
976 });
977
978 let before_create_override = input.before_create.map(|closure| {
979 let param = closure
980 .inputs
981 .first()
982 .expect("before_create closure must have exactly one parameter");
983 let body = &closure.body;
984 quote::quote! {
985 fn before_create(&mut self) {
986 let hook = |#param: &mut Self| #body;
987 hook(self);
988 }
989 }
990 });
991
992 let before_update_override = input.before_update.map(|closure| {
993 let param = closure
994 .inputs
995 .first()
996 .expect("before_update closure must have exactly one parameter");
997 let body = &closure.body;
998 quote::quote! {
999 fn before_update(&mut self) {
1000 let hook = |#param: &mut Self| #body;
1001 hook(self);
1002 }
1003 }
1004 });
1005
1006 quote::quote! {
1007 #[derive(::std::fmt::Debug, ::std::clone::Clone, cinderblock_core::serde::Serialize, cinderblock_core::serde::Deserialize)]
1008 pub struct #ident {
1009 #(#fields),*
1010 }
1011
1012 impl cinderblock_core::Resource for #ident {
1013 type PrimaryKey = #primary_key_type;
1014
1015 type DataLayer = #data_layer_path;
1016
1017 const NAME: &'static [&'static str] = &[#(#name_segments),*];
1018
1019 const RESOURCE_NAME: &'static str = #resource_name_literal;
1020
1021 const PRIMARY_KEY_GENERATED: bool = #primary_key_generated;
1022
1023 fn primary_key(&self) -> &Self::PrimaryKey {
1024 #primary_key_value
1025 }
1026
1027 #before_create_override
1028
1029 #before_update_override
1030 }
1031
1032 #(#actions)*
1033
1034 #(#extension_calls)*
1035 }
1036 .into()
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041 use super::*;
1042 use assert2::{assert, check};
1043 use cinderblock_extension_api::ResourceActionInput;
1044 use quote::quote;
1045
1046 fn parse_resource(tokens: proc_macro2::TokenStream) -> ResourceMacroInput {
1047 let result = syn::parse2::<ResourceMacroInput>(tokens);
1048 assert!(let Ok(input) = result);
1049 input
1050 }
1051
1052 #[test]
1053 fn minimal_resource_with_one_simple_attribute() {
1054 let input = parse_resource(quote! {
1055 name = Foo;
1056
1057 attributes {
1058 id String;
1059 }
1060 });
1061
1062 check!(input.name.len() == 1);
1063 check!(input.name[0] == "Foo");
1064
1065 check!(input.attributes.len() == 1);
1066 let attr = &input.attributes[0];
1067 check!(attr.name == "id");
1068 check!(!attr.primary_key.value());
1069 check!(!attr.generated.value());
1070 check!(attr.writable.value());
1071 check!(attr.default.is_none());
1072
1073 check!(input.actions.is_empty());
1074 }
1075
1076 #[test]
1077 fn dotted_name_parses_into_multiple_segments() {
1078 let input = parse_resource(quote! {
1079 name = Helpdesk.Support.Ticket;
1080
1081 attributes {
1082 id String;
1083 }
1084 });
1085
1086 check!(input.name.len() == 3);
1087 check!(input.name[0] == "Helpdesk");
1088 check!(input.name[1] == "Support");
1089 check!(input.name[2] == "Ticket");
1090 }
1091
1092 #[test]
1093 fn attribute_with_options_block() {
1094 let input = parse_resource(quote! {
1095 name = Ticket;
1096
1097 attributes {
1098 ticket_id Uuid {
1099 primary_key true;
1100 writable false;
1101 default || uuid::Uuid::new_v4();
1102 }
1103 }
1104 });
1105
1106 check!(input.attributes.len() == 1);
1107 let attr = &input.attributes[0];
1108 check!(attr.name == "ticket_id");
1109 check!(attr.primary_key.value());
1110 check!(!attr.writable.value());
1111 check!(attr.default.is_some());
1112 }
1113
1114 #[test]
1115 fn attribute_with_generated_flag() {
1116 let input = parse_resource(quote! {
1117 name = Item;
1118
1119 attributes {
1120 item_id Uuid {
1121 primary_key true;
1122 generated true;
1123 }
1124 }
1125 });
1126
1127 let attr = &input.attributes[0];
1128 check!(attr.generated.value());
1129 check!(attr.primary_key.value());
1130 check!(attr.writable.value());
1132 }
1133
1134 #[test]
1135 fn multiple_attributes_mixed_simple_and_complex() {
1136 let input = parse_resource(quote! {
1137 name = Order;
1138
1139 attributes {
1140 order_id Uuid {
1141 primary_key true;
1142 writable false;
1143 }
1144 item_name String;
1145 quantity u32;
1146 }
1147 });
1148
1149 check!(input.attributes.len() == 3);
1150
1151 check!(input.attributes[0].name == "order_id");
1152 check!(input.attributes[0].primary_key.value());
1153 check!(!input.attributes[0].writable.value());
1154
1155 check!(input.attributes[1].name == "item_name");
1156 check!(!input.attributes[1].primary_key.value());
1157 check!(input.attributes[1].writable.value());
1158
1159 check!(input.attributes[2].name == "quantity");
1160 check!(!input.attributes[2].primary_key.value());
1161 check!(input.attributes[2].writable.value());
1162 }
1163
1164 #[test]
1165 fn actions_block_with_simple_create() {
1166 let input = parse_resource(quote! {
1167 name = Ticket;
1168
1169 attributes {
1170 id String;
1171 }
1172
1173 actions {
1174 create open;
1175 }
1176 });
1177
1178 check!(input.actions.len() == 1);
1179 check!(input.actions[0].name == "open");
1180 check!(let ResourceActionInputKind::Create { accept: Accept::Default } = &input.actions[0].kind);
1181 }
1182
1183 #[test]
1184 fn action_with_accept_list() {
1185 let input = parse_resource(quote! {
1186 name = Ticket;
1187
1188 attributes {
1189 id String;
1190 }
1191
1192 actions {
1193 create assign {
1194 accept [subject];
1195 };
1196 }
1197 });
1198
1199 check!(input.actions.len() == 1);
1200 check!(input.actions[0].name == "assign");
1201 assert!(let ResourceActionInputKind::Create { accept: Accept::Only(idents) } = &input.actions[0].kind);
1202 check!(idents.len() == 1);
1203 check!(idents[0] == "subject");
1204 }
1205
1206 #[test]
1207 fn no_actions_block_omitted() {
1208 let input = parse_resource(quote! {
1209 name = Simple;
1210
1211 attributes {
1212 id u64;
1213 }
1214 });
1215
1216 check!(input.actions.is_empty());
1217 }
1218
1219 #[test]
1220 fn full_helpdesk_example() {
1221 let input = parse_resource(quote! {
1222 name = Helpdesk.Support.Ticket;
1223
1224 attributes {
1225 ticket_id Uuid {
1226 primary_key true;
1227 writable false;
1228 default || uuid::Uuid::new_v4();
1229 }
1230
1231 subject String;
1232
1233 status TicketStatus;
1234 }
1235
1236 actions {
1237 create open;
1238
1239 create assign {
1240 accept [subject];
1241 };
1242 }
1243 });
1244
1245 check!(input.name.len() == 3);
1246 check!(input.name[0] == "Helpdesk");
1247 check!(input.name[1] == "Support");
1248 check!(input.name[2] == "Ticket");
1249
1250 check!(input.attributes.len() == 3);
1251
1252 let ticket_id = &input.attributes[0];
1253 check!(ticket_id.name == "ticket_id");
1254 check!(ticket_id.primary_key.value());
1255 check!(!ticket_id.writable.value());
1256 check!(ticket_id.default.is_some());
1257
1258 let subject = &input.attributes[1];
1259 check!(subject.name == "subject");
1260 check!(!subject.primary_key.value());
1261 check!(subject.writable.value());
1262 check!(subject.default.is_none());
1263
1264 let status = &input.attributes[2];
1265 check!(status.name == "status");
1266 check!(!status.primary_key.value());
1267 check!(status.writable.value());
1268
1269 check!(input.actions.len() == 2);
1270
1271 check!(input.actions[0].name == "open");
1272 check!(let ResourceActionInputKind::Create { accept: Accept::Default } = &input.actions[0].kind);
1273
1274 check!(input.actions[1].name == "assign");
1275 assert!(let ResourceActionInputKind::Create { accept: Accept::Only(idents) } = &input.actions[1].kind);
1276 check!(idents.len() == 1);
1277 check!(idents[0] == "subject");
1278 }
1279
1280 #[test]
1281 fn parse_simple_create_action() {
1282 assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
1283 create open;
1284 }));
1285
1286 check!(action.name == "open");
1287 check!(let ResourceActionInputKind::Create { accept: Accept::Default } = action.kind);
1288 }
1289
1290 #[test]
1291 fn parse_create_action_with_multiple_accept_idents() {
1292 assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
1293 create bulk_insert {
1294 accept [name, email, age];
1295 }
1296 }));
1297
1298 check!(action.name == "bulk_insert");
1299 assert!(let ResourceActionInputKind::Create { accept: Accept::Only(idents) } = action.kind);
1300 let names: Vec<String> = idents.iter().map(|i| i.to_string()).collect();
1301 check!(names == vec!["name", "email", "age"]);
1302 }
1303
1304 #[test]
1305 fn unknown_action_kind_produces_error() {
1306 let result = syn::parse2::<ResourceActionInput>(quote! {
1307 frobnicate foo;
1308 });
1309
1310 assert!(let Err(err) = result);
1311 let msg = err.to_string();
1312 check!(msg.contains("Unexpected action kind"));
1313 check!(msg.contains("frobnicate"));
1314 }
1315
1316 #[test]
1317 fn unknown_attribute_option_produces_error() {
1318 let result = syn::parse2::<ResourceMacroInput>(quote! {
1319 name = Thing;
1320
1321 attributes {
1322 id String {
1323 bogus true;
1324 }
1325 }
1326 });
1327
1328 assert!(let Err(err) = result);
1329 let msg = err.to_string();
1330 check!(msg.contains("Unexpected attribute key"));
1331 check!(msg.contains("bogus"));
1332 }
1333
1334 #[test]
1335 fn missing_semicolon_after_name_produces_error() {
1336 let result = syn::parse2::<ResourceMacroInput>(quote! {
1337 name = Foo
1338
1339 attributes {
1340 id String;
1341 }
1342 });
1343
1344 check!(let Err(_) = result);
1345 }
1346
1347 #[test]
1348 fn parse_simple_update_action_with_default_accept() {
1349 assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
1350 update close;
1351 }));
1352
1353 check!(action.name == "close");
1354 assert!(let ResourceActionInputKind::Update(update) = &action.kind);
1355 check!(let Accept::Default = update.accept);
1356 check!(update.changes.is_empty());
1357 }
1358
1359 #[test]
1360 fn parse_update_action_with_accept_and_change_ref() {
1361 assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
1362 update close {
1363 accept [];
1364 change_ref |resource| {
1365 resource.status = TicketStatus::Closed;
1366 };
1367 }
1368 }));
1369
1370 check!(action.name == "close");
1371 assert!(let ResourceActionInputKind::Update(update) = &action.kind);
1372 assert!(let Accept::Only(idents) = &update.accept);
1373 check!(idents.is_empty());
1374 check!(update.changes.len() == 1);
1375 check!(let UpdateChange::ChangeRef(_) = &update.changes[0]);
1376 }
1377
1378 #[test]
1379 fn parse_update_action_with_accept_fields() {
1380 assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
1381 update reassign {
1382 accept [subject, status];
1383 }
1384 }));
1385
1386 check!(action.name == "reassign");
1387 assert!(let ResourceActionInputKind::Update(update) = &action.kind);
1388 assert!(let Accept::Only(idents) = &update.accept);
1389 let names: Vec<String> = idents.iter().map(|i| i.to_string()).collect();
1390 check!(names == vec!["subject", "status"]);
1391 check!(update.changes.is_empty());
1392 }
1393}