1#![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 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 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 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 let schema_type_name = &schema_type_names[0];
493 fields.push(quote! {
494 pub schemas: [#schema_type_name; 1],
495 });
496 } else {
497 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#[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 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}