1mod attribute_parser;
2mod attributes;
3
4mod field_analyzer;
5mod macro_implementation;
6mod relation_validator;
7mod structs;
8mod two_pass_generator;
9
10#[cfg(feature = "debug")]
11mod debug_output;
12
13use proc_macro::TokenStream;
14
15use heck::ToPascalCase;
16use quote::{ToTokens, format_ident, quote};
17use syn::parse::Parser;
18use syn::{
19 Data, DeriveInput, Fields, Lit, Meta, parse_macro_input, punctuated::Punctuated, token::Comma,
20};
21
22use structs::{CRUDResourceMeta, EntityFieldAnalysis};
23
24fn extract_active_model_type(input: &DeriveInput, name: &syn::Ident) -> proc_macro2::TokenStream {
29 let mut active_model_override = None;
30 for attr in &input.attrs {
31 if attr.path().is_ident("active_model")
32 && let Some(s) = attribute_parser::get_string_from_attr(attr)
33 {
34 active_model_override =
35 Some(syn::parse_str::<syn::Type>(&s).expect("Invalid active_model type"));
36 }
37 }
38 if let Some(ty) = active_model_override {
39 quote! { #ty }
40 } else {
41 let ident = format_ident!("{}ActiveModel", name);
42 quote! { #ident }
43 }
44}
45
46fn extract_named_fields(
47 input: &DeriveInput,
48) -> syn::punctuated::Punctuated<syn::Field, syn::token::Comma> {
49 if let Data::Struct(data) = &input.data {
50 if let Fields::Named(named) = &data.fields {
51 named.named.clone()
52 } else {
53 panic!("ToCreateModel only supports structs with named fields");
54 }
55 } else {
56 panic!("ToCreateModel can only be derived for structs");
57 }
58}
59
60fn generate_update_merge_code(
61 fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
62 included_fields: &[&syn::Field],
63) -> (Vec<proc_macro2::TokenStream>, Vec<proc_macro2::TokenStream>) {
64 let included_merge = generate_included_merge_code(included_fields);
65 let excluded_merge = generate_excluded_merge_code(fields);
66 (included_merge, excluded_merge)
67}
68
69fn generate_included_merge_code(included_fields: &[&syn::Field]) -> Vec<proc_macro2::TokenStream> {
70 included_fields
71 .iter()
72 .filter(|field| {
73 !attribute_parser::get_crudcrate_bool(field, "non_db_attr").unwrap_or(false)
74 })
75 .map(|field| {
76 let ident = &field.ident;
77 let is_optional = field_analyzer::field_is_optional(field);
78
79 if is_optional {
80 quote! {
81 model.#ident = match self.#ident {
82 Some(Some(value)) => sea_orm::ActiveValue::Set(Some(value.into())),
83 Some(None) => sea_orm::ActiveValue::Set(None),
84 None => sea_orm::ActiveValue::NotSet,
85 };
86 }
87 } else {
88 quote! {
89 model.#ident = match self.#ident {
90 Some(Some(value)) => sea_orm::ActiveValue::Set(value.into()),
91 Some(None) => {
92 return Err(sea_orm::DbErr::Custom(format!(
93 "Field '{}' is required and cannot be set to null",
94 stringify!(#ident)
95 )));
96 },
97 None => sea_orm::ActiveValue::NotSet,
98 };
99 }
100 }
101 })
102 .collect()
103}
104
105fn generate_excluded_merge_code(
106 fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
107) -> Vec<proc_macro2::TokenStream> {
108 fields
109 .iter()
110 .filter(|field| {
111 attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false)
112 && !attribute_parser::get_crudcrate_bool(field, "non_db_attr").unwrap_or(false)
113 })
114 .filter_map(|field| {
115 if let Some(expr) = attribute_parser::get_crudcrate_expr(field, "on_update") {
116 let ident = &field.ident;
117 if field_analyzer::field_is_optional(field) {
118 Some(quote! {
119 model.#ident = sea_orm::ActiveValue::Set(Some((#expr).into()));
120 })
121 } else {
122 Some(quote! {
123 model.#ident = sea_orm::ActiveValue::Set((#expr).into());
124 })
125 }
126 } else {
127 None
128 }
129 })
130 .collect()
131}
132
133fn extract_entity_fields(
134 input: &DeriveInput,
135) -> Result<&syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, TokenStream> {
136 match &input.data {
137 Data::Struct(data) => match &data.fields {
138 Fields::Named(fields) => Ok(&fields.named),
139 _ => Err(syn::Error::new_spanned(
140 input,
141 "EntityToModels only supports structs with named fields",
142 )
143 .to_compile_error()
144 .into()),
145 },
146 _ => Err(
147 syn::Error::new_spanned(input, "EntityToModels only supports structs")
148 .to_compile_error()
149 .into(),
150 ),
151 }
152}
153
154fn parse_entity_attributes(input: &DeriveInput, struct_name: &syn::Ident) -> (syn::Ident, String) {
155 let mut api_struct_name = None;
156 let mut active_model_path = None;
157
158 for attr in &input.attrs {
159 if attr.path().is_ident("crudcrate")
160 && let Meta::List(meta_list) = &attr.meta
161 && let Ok(metas) =
162 Punctuated::<Meta, Comma>::parse_terminated.parse2(meta_list.tokens.clone())
163 {
164 for meta in &metas {
165 if let Meta::NameValue(nv) = meta {
166 if nv.path.is_ident("api_struct") {
167 if let syn::Expr::Lit(expr_lit) = &nv.value
168 && let Lit::Str(s) = &expr_lit.lit
169 {
170 api_struct_name = Some(format_ident!("{}", s.value()));
171 }
172 } else if nv.path.is_ident("active_model")
173 && let syn::Expr::Lit(expr_lit) = &nv.value
174 && let Lit::Str(s) = &expr_lit.lit
175 {
176 active_model_path = Some(s.value());
177 }
178 }
179 }
180 }
181 }
182
183 let table_name = attribute_parser::extract_table_name(&input.attrs)
184 .unwrap_or_else(|| struct_name.to_string());
185 let api_struct_name =
186 api_struct_name.unwrap_or_else(|| format_ident!("{}", table_name.to_pascal_case()));
187 let active_model_path = active_model_path.unwrap_or_else(|| "ActiveModel".to_string());
188
189 (api_struct_name, active_model_path)
190}
191
192fn analyze_entity_fields(
193 fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
194) -> EntityFieldAnalysis<'_> {
195 let mut analysis = EntityFieldAnalysis {
196 db_fields: Vec::new(),
197 non_db_fields: Vec::new(),
198 primary_key_field: None,
199 sortable_fields: Vec::new(),
200 filterable_fields: Vec::new(),
201 fulltext_fields: Vec::new(),
202 join_on_one_fields: Vec::new(),
203 join_on_all_fields: Vec::new(),
204 };
206
207 for field in fields {
208 let is_non_db = attribute_parser::get_crudcrate_bool(field, "non_db_attr").unwrap_or(false);
209
210 if let Some(join_config) = attribute_parser::get_join_config(field) {
212 if join_config.on_one {
213 analysis.join_on_one_fields.push(field);
214 }
215 if join_config.on_all {
216 analysis.join_on_all_fields.push(field);
217 }
218 }
220
221 if is_non_db {
222 analysis.non_db_fields.push(field);
223 } else {
224 analysis.db_fields.push(field);
225
226 if attribute_parser::field_has_crudcrate_flag(field, "primary_key") {
227 analysis.primary_key_field = Some(field);
228 }
229 if attribute_parser::field_has_crudcrate_flag(field, "sortable") {
230 analysis.sortable_fields.push(field);
231 }
232 if attribute_parser::field_has_crudcrate_flag(field, "filterable") {
233 analysis.filterable_fields.push(field);
234 }
235 if attribute_parser::field_has_crudcrate_flag(field, "fulltext") {
236 analysis.fulltext_fields.push(field);
237 }
238 }
239 }
240
241 analysis
242}
243
244fn validate_field_analysis(analysis: &EntityFieldAnalysis) -> Result<(), TokenStream> {
245 if analysis.primary_key_field.is_some()
246 && analysis
247 .db_fields
248 .iter()
249 .filter(|field| attribute_parser::field_has_crudcrate_flag(field, "primary_key"))
250 .count()
251 > 1
252 {
253 return Err(syn::Error::new_spanned(
254 analysis.primary_key_field.unwrap(),
255 "Only one field can be marked with 'primary_key' attribute",
256 )
257 .to_compile_error()
258 .into());
259 }
260
261 for field in &analysis.non_db_fields {
263 if !has_sea_orm_ignore(field) {
264 let field_name = field
265 .ident
266 .as_ref()
267 .map_or_else(|| "unknown".to_string(), std::string::ToString::to_string);
268 return Err(syn::Error::new_spanned(
269 field,
270 format!(
271 "Field '{field_name}' has #[crudcrate(non_db_attr)] but is missing #[sea_orm(ignore)].\n\
272 Non-database fields must be marked with both attributes.\n\
273 Add #[sea_orm(ignore)] above the #[crudcrate(...)] attribute."
274 ),
275 )
276 .to_compile_error()
277 .into());
278 }
279 }
280
281 Ok(())
282}
283
284fn has_sea_orm_ignore(field: &syn::Field) -> bool {
286 for attr in &field.attrs {
287 if attr.path().is_ident("sea_orm")
288 && let Meta::List(meta_list) = &attr.meta
289 && let Ok(metas) =
290 Punctuated::<Meta, Comma>::parse_terminated.parse2(meta_list.tokens.clone())
291 {
292 for meta in metas {
293 if let Meta::Path(path) = meta
294 && path.is_ident("ignore")
295 {
296 return true;
297 }
298 }
299 }
300 }
301 false
302}
303
304fn resolve_join_field_type_preserving_container(
305 field_type: &syn::Type,
306) -> proc_macro2::TokenStream {
307 if let Some(base_type_str) = two_pass_generator::extract_base_type_string(field_type) {
309 if let Some(api_name) = two_pass_generator::find_api_struct_name(&base_type_str) {
311 let api_struct_ident = quote::format_ident!("{}", api_name);
312
313 if let syn::Type::Path(type_path) = field_type
315 && let Some(segment) = type_path.path.segments.last()
316 {
317 if segment.ident == "Vec" {
318 return quote! { Vec<#api_struct_ident> };
319 } else if segment.ident == "Option" {
320 return quote! { Option<#api_struct_ident> };
321 }
322 }
323
324 return quote! { #api_struct_ident };
325 }
326 }
327
328 #[cfg(feature = "debug")]
330 {
331 eprintln!(
332 "WARNING: Could not resolve join field type, keeping as-is: {:?}",
333 quote! { #field_type }
334 );
335 }
336 quote! { #field_type }
337}
338
339fn generate_api_struct_content(
340 analysis: &EntityFieldAnalysis,
341 _api_struct_name: &syn::Ident,
342) -> (
343 Vec<proc_macro2::TokenStream>,
344 Vec<proc_macro2::TokenStream>,
345 std::collections::HashSet<String>,
346) {
347 let mut api_struct_fields = Vec::new();
348 let mut from_model_assignments = Vec::new();
349 let required_imports = std::collections::HashSet::new();
350
351 for field in &analysis.db_fields {
352 let field_name = &field.ident;
353 let field_type = &field.ty;
354
355 let api_field_attrs: Vec<_> = field
356 .attrs
357 .iter()
358 .filter(|attr| !attr.path().is_ident("sea_orm"))
359 .collect();
360
361 api_struct_fields.push(quote! {
362 #(#api_field_attrs)*
363 pub #field_name: #field_type
364 });
365
366 let assignment = if field_type
368 .to_token_stream()
369 .to_string()
370 .contains("DateTimeWithTimeZone")
371 {
372 if field_analyzer::field_is_optional(field) {
373 quote! {
374 #field_name: model.#field_name.map(|dt| dt.with_timezone(&chrono::Utc))
375 }
376 } else {
377 quote! {
378 #field_name: model.#field_name.with_timezone(&chrono::Utc)
379 }
380 }
381 } else {
382 quote! {
383 #field_name: model.#field_name
384 }
385 };
386
387 from_model_assignments.push(assignment);
388 }
389
390 for field in &analysis.non_db_fields {
391 let field_name = &field.ident;
392 let field_type = &field.ty;
393
394 let default_expr = attribute_parser::get_crudcrate_expr(field, "default")
395 .unwrap_or_else(|| syn::parse_quote!(Default::default()));
396
397 let crudcrate_attrs: Vec<_> = field
399 .attrs
400 .iter()
401 .filter(|attr| attr.path().is_ident("crudcrate"))
402 .collect();
403
404 let schema_attrs = if attribute_parser::get_join_config(field).is_some() {
407 quote! { #[schema(no_recursion)] }
408 } else {
409 quote! {}
410 };
411
412 let final_field_type = if attribute_parser::get_join_config(field).is_some() {
413 resolve_join_field_type_preserving_container(field_type)
414 } else {
415 quote! { #field_type }
416 };
417
418 let field_definition = quote! {
419 #schema_attrs
420 #(#crudcrate_attrs)*
421 pub #field_name: #final_field_type
422 };
423
424 api_struct_fields.push(field_definition);
425
426 let assignment = if attribute_parser::get_join_config(field).is_some() {
427 let empty_value = if let Ok(syn::Type::Path(type_path)) =
428 syn::parse2::<syn::Type>(quote! { #final_field_type })
429 {
430 if let Some(segment) = type_path.path.segments.last() {
431 if segment.ident == "Vec" {
432 quote! { vec![] }
433 } else if segment.ident == "Option" {
434 quote! { None }
435 } else {
436 quote! { Default::default() }
437 }
438 } else {
439 quote! { Default::default() }
440 }
441 } else {
442 quote! { Default::default() }
443 };
444
445 quote! {
446 #field_name: #empty_value
447 }
448 } else {
449 quote! {
450 #field_name: #default_expr
451 }
452 };
453
454 from_model_assignments.push(assignment);
455 }
456
457 (api_struct_fields, from_model_assignments, required_imports)
458}
459
460fn generate_api_struct(
461 api_struct_name: &syn::Ident,
462 api_struct_fields: &[proc_macro2::TokenStream],
463 active_model_path: &str,
464 crud_meta: &structs::CRUDResourceMeta,
465 analysis: &EntityFieldAnalysis,
466 _required_imports: &std::collections::HashSet<String>,
467) -> proc_macro2::TokenStream {
468 let _has_create_exclusions = analysis
470 .db_fields
471 .iter()
472 .chain(analysis.non_db_fields.iter())
473 .any(|field| attribute_parser::get_crudcrate_bool(field, "create_model") == Some(false));
474 let _has_update_exclusions = analysis
475 .db_fields
476 .iter()
477 .chain(analysis.non_db_fields.iter())
478 .any(|field| attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false));
479
480 let has_join_fields =
482 !analysis.join_on_one_fields.is_empty() || !analysis.join_on_all_fields.is_empty();
483
484 let has_fields_needing_default = has_join_fields
486 || analysis.non_db_fields.iter().any(|field| {
487 attribute_parser::get_crudcrate_bool(field, "create_model") == Some(false)
489 || attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false)
490 })
491 || analysis.db_fields.iter().any(|field| {
492 attribute_parser::get_crudcrate_bool(field, "create_model") == Some(false)
494 || attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false)
495 });
496
497 let mut derives = vec![
499 quote!(Clone),
500 quote!(Debug),
501 quote!(Serialize),
502 quote!(Deserialize),
503 quote!(ToCreateModel),
504 quote!(ToUpdateModel),
505 ];
506
507 derives.push(quote!(ToSchema));
510
511 if has_fields_needing_default && !has_join_fields {
515 derives.push(quote!(Default));
516 }
517
518 if crud_meta.derive_partial_eq {
519 derives.push(quote!(PartialEq));
520 }
521
522 if crud_meta.derive_eq {
523 derives.push(quote!(Eq));
524 }
525
526 let mut import_statements: Vec<proc_macro2::TokenStream> = vec![];
528 let mut seen_imports: std::collections::HashSet<String> = std::collections::HashSet::new();
529
530 let all_join_fields: Vec<_> = analysis
532 .join_on_one_fields
533 .iter()
534 .chain(analysis.join_on_all_fields.iter())
535 .collect();
536
537 for field in all_join_fields {
538 if let Some(base_type_str) = two_pass_generator::extract_base_type_string(&field.ty)
539 && let Some(api_name) = two_pass_generator::find_api_struct_name(&base_type_str)
540 {
541 fn extract_innermost_path(ty: &syn::Type) -> Option<&syn::TypePath> {
542 if let syn::Type::Path(type_path) = ty {
543 if let Some(segment) = type_path.path.segments.last()
544 && (segment.ident == "Vec" || segment.ident == "Option")
545 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
546 && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
547 {
548 return extract_innermost_path(inner_ty);
550 }
551 return Some(type_path);
553 }
554 None
555 }
556
557 if let Some(inner_type_path) = extract_innermost_path(&field.ty) {
558 let path_segments: Vec<_> = inner_type_path
561 .path
562 .segments
563 .iter()
564 .take(inner_type_path.path.segments.len() - 1) .map(|seg| seg.ident.clone())
566 .collect();
567
568 if !path_segments.is_empty() {
569 let api_ident = quote::format_ident!("{}", api_name);
570 let module_path = quote! { #(#path_segments)::* };
571
572 let import_key = format!("{}::{}", quote! {#module_path}, api_name);
574
575 if !seen_imports.contains(&import_key) {
576 seen_imports.insert(import_key);
577
578 let import_stmt = quote! {
579 use #module_path::#api_ident;
580 };
581
582 import_statements.push(import_stmt);
583 }
584 }
585 }
586 }
587 }
588
589 quote! {
590 use sea_orm::ActiveValue;
591 use utoipa::ToSchema;
592 use serde::{Serialize, Deserialize};
593 use crudcrate::{ToUpdateModel, ToCreateModel};
594 #(#import_statements)*
595
596 #[derive(#(#derives),*)]
597 #[active_model = #active_model_path]
598 pub struct #api_struct_name {
599 #(#api_struct_fields),*
600 }
601 }
602}
603
604fn generate_from_impl(
605 struct_name: &syn::Ident,
606 api_struct_name: &syn::Ident,
607 from_model_assignments: &[proc_macro2::TokenStream],
608) -> proc_macro2::TokenStream {
609 quote! {
610 impl From<#struct_name> for #api_struct_name {
611 fn from(model: #struct_name) -> Self {
612 Self {
613 #(#from_model_assignments),*
614 }
615 }
616 }
617 }
618}
619
620fn generate_conditional_crud_impl(
621 api_struct_name: &syn::Ident,
622 crud_meta: &CRUDResourceMeta,
623 active_model_path: &str,
624 analysis: &EntityFieldAnalysis,
625 table_name: &str,
626) -> proc_macro2::TokenStream {
627 let has_crud_resource_fields = analysis.primary_key_field.is_some()
630 || !analysis.sortable_fields.is_empty()
631 || !analysis.filterable_fields.is_empty()
632 || !analysis.fulltext_fields.is_empty();
633
634 let crud_impl = if has_crud_resource_fields {
635 macro_implementation::generate_crud_resource_impl(
636 api_struct_name,
637 crud_meta,
638 active_model_path,
639 analysis,
640 table_name,
641 )
642 } else {
643 quote! {}
644 };
645
646 let router_impl = if crud_meta.generate_router && has_crud_resource_fields {
647 macro_implementation::generate_router_impl(api_struct_name)
648 } else {
649 quote! {}
650 };
651
652 quote! {
653 #crud_impl
654 #router_impl
655 }
656}
657
658#[proc_macro_derive(ToCreateModel, attributes(crudcrate, active_model))]
691pub fn to_create_model(input: TokenStream) -> TokenStream {
692 let input = parse_macro_input!(input as DeriveInput);
693 let name = &input.ident;
694 let create_name = format_ident!("{}Create", name);
695
696 let active_model_type = extract_active_model_type(&input, name);
697 let fields = extract_named_fields(&input);
698 let create_struct_fields = macro_implementation::generate_create_struct_fields(&fields);
699 let conv_lines = macro_implementation::generate_create_conversion_lines(&fields);
700
701 let create_derives =
704 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
705
706 let expanded = quote! {
707 #[derive(#create_derives)]
708 pub struct #create_name {
709 #(#create_struct_fields),*
710 }
711
712 impl From<#create_name> for #active_model_type {
713 fn from(create: #create_name) -> Self {
714 #active_model_type {
715 #(#conv_lines),*
716 }
717 }
718 }
719 };
720
721 #[cfg(feature = "debug")]
722 debug_output::print_create_model_debug(&expanded, &name.to_string());
723
724 TokenStream::from(expanded)
725}
726
727#[proc_macro_derive(ToUpdateModel, attributes(crudcrate, active_model))]
753pub fn to_update_model(input: TokenStream) -> TokenStream {
754 let input = parse_macro_input!(input as DeriveInput);
755 let name = &input.ident;
756 let update_name = format_ident!("{}Update", name);
757
758 let active_model_type = extract_active_model_type(&input, name);
759 let fields = extract_named_fields(&input);
760 let included_fields = macro_implementation::filter_update_fields(&fields);
761 let update_struct_fields =
762 macro_implementation::generate_update_struct_fields(&included_fields);
763 let (included_merge, excluded_merge) = generate_update_merge_code(&fields, &included_fields);
764
765 let update_derives =
768 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
769
770 let expanded = quote! {
771 #[derive(#update_derives)]
772 pub struct #update_name {
773 #(#update_struct_fields),*
774 }
775
776 impl #update_name {
777 pub fn merge_fields(self, mut model: #active_model_type) -> Result<#active_model_type, sea_orm::DbErr> {
778 #(#included_merge)*
779 #(#excluded_merge)*
780 Ok(model)
781 }
782 }
783
784 impl crudcrate::traits::MergeIntoActiveModel<#active_model_type> for #update_name {
785 fn merge_into_activemodel(self, model: #active_model_type) -> Result<#active_model_type, sea_orm::DbErr> {
786 Self::merge_fields(self, model)
787 }
788 }
789 };
790
791 #[cfg(feature = "debug")]
792 debug_output::print_update_model_debug(&expanded, &name.to_string());
793
794 TokenStream::from(expanded)
795}
796
797#[proc_macro_derive(ToListModel, attributes(crudcrate))]
835pub fn to_list_model(input: TokenStream) -> TokenStream {
836 let input = parse_macro_input!(input as DeriveInput);
837 let name = &input.ident;
838 let list_name = format_ident!("{}List", name);
839
840 let fields = extract_named_fields(&input);
841 let list_struct_fields = macro_implementation::generate_list_struct_fields(&fields);
842 let list_from_assignments = macro_implementation::generate_list_from_assignments(&fields);
843
844 let list_derives = quote! { Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
847
848 let expanded = quote! {
849 #[derive(#list_derives)]
850 pub struct #list_name {
851 #(#list_struct_fields),*
852 }
853
854 impl From<#name> for #list_name {
855 fn from(model: #name) -> Self {
856 Self {
857 #(#list_from_assignments),*
858 }
859 }
860 }
861 };
862
863 TokenStream::from(expanded)
864}
865
866fn parse_and_validate_entity_attributes(
1029 input: &DeriveInput,
1030 struct_name: &syn::Ident,
1031) -> Result<(String, syn::Ident, String, CRUDResourceMeta), TokenStream> {
1032 let (api_struct_name, active_model_path) = parse_entity_attributes(input, struct_name);
1033 let table_name = attribute_parser::extract_table_name(&input.attrs)
1034 .unwrap_or_else(|| struct_name.to_string());
1035
1036 let crud_meta = match attribute_parser::parse_crud_resource_meta(&input.attrs) {
1037 Ok(meta) => meta.with_defaults(&table_name, &api_struct_name.to_string()),
1038 Err(e) => return Err(e.to_compile_error().into()),
1039 };
1040
1041 if syn::parse_str::<syn::Type>(&active_model_path).is_err() {
1043 return Err(syn::Error::new_spanned(
1044 input,
1045 format!("Invalid active_model path: {active_model_path}"),
1046 )
1047 .to_compile_error()
1048 .into());
1049 }
1050
1051 Ok((
1052 table_name,
1053 api_struct_name,
1054 active_model_path,
1055 crud_meta,
1056 ))
1057}
1058
1059fn setup_join_validation(
1061 field_analysis: &EntityFieldAnalysis,
1062 api_struct_name: &syn::Ident,
1063) -> Result<proc_macro2::TokenStream, TokenStream> {
1064 let entity_name = api_struct_name.to_string();
1066 two_pass_generator::register_entity_globally(&entity_name, &entity_name);
1067
1068 let _join_validation = relation_validator::generate_join_relation_validation(field_analysis);
1070
1071 let cyclic_dependency_check = relation_validator::generate_cyclic_dependency_check(
1073 field_analysis,
1074 &api_struct_name.to_string(),
1075 );
1076 if !cyclic_dependency_check.is_empty() {
1077 return Err(cyclic_dependency_check.into());
1078 }
1079
1080 Ok(quote! {})
1081}
1082
1083fn generate_core_api_models(
1085 struct_name: &syn::Ident,
1086 api_struct_name: &syn::Ident,
1087 crud_meta: &CRUDResourceMeta,
1088 active_model_path: &str,
1089 field_analysis: &EntityFieldAnalysis,
1090 table_name: &str,
1091) -> (proc_macro2::TokenStream, proc_macro2::TokenStream, proc_macro2::TokenStream) {
1092 let (api_struct_fields, from_model_assignments, required_imports) =
1093 generate_api_struct_content(field_analysis, api_struct_name);
1094 let api_struct = generate_api_struct(
1095 api_struct_name,
1096 &api_struct_fields,
1097 active_model_path,
1098 crud_meta,
1099 field_analysis,
1100 &required_imports,
1101 );
1102 let from_impl = generate_from_impl(struct_name, api_struct_name, &from_model_assignments);
1103 let crud_impl = generate_conditional_crud_impl(
1104 api_struct_name,
1105 crud_meta,
1106 active_model_path,
1107 field_analysis,
1108 table_name,
1109 );
1110
1111 (api_struct, from_impl, crud_impl)
1112}
1113
1114fn generate_list_and_response_models(
1116 input: &DeriveInput,
1117 api_struct_name: &syn::Ident,
1118 struct_name: &syn::Ident,
1119 field_analysis: &EntityFieldAnalysis,
1120) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
1121 let list_name = format_ident!("{}List", api_struct_name);
1123 let raw_fields = extract_named_fields(input);
1124 let list_struct_fields = macro_implementation::generate_list_struct_fields(&raw_fields);
1125 let list_from_assignments = macro_implementation::generate_list_from_assignments(&raw_fields);
1126 let list_from_model_assignments =
1127 macro_implementation::generate_list_from_model_assignments(field_analysis);
1128
1129 let list_derives =
1130 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
1131
1132 let list_model = quote! {
1133 #[derive(#list_derives)]
1134 pub struct #list_name {
1135 #(#list_struct_fields),*
1136 }
1137
1138 impl From<#api_struct_name> for #list_name {
1139 fn from(model: #api_struct_name) -> Self {
1140 Self {
1141 #(#list_from_assignments),*
1142 }
1143 }
1144 }
1145
1146 impl From<#struct_name> for #list_name {
1147 fn from(model: #struct_name) -> Self {
1148 Self {
1149 #(#list_from_model_assignments),*
1150 }
1151 }
1152 }
1153 };
1154
1155 let response_name = format_ident!("{}Response", api_struct_name);
1157 let response_struct_fields = macro_implementation::generate_response_struct_fields(&raw_fields);
1158 let response_from_assignments =
1159 macro_implementation::generate_response_from_assignments(&raw_fields);
1160
1161 let response_derives =
1162 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
1163
1164 let response_model = quote! {
1165 #[derive(#response_derives)]
1166 pub struct #response_name {
1167 #(#response_struct_fields),*
1168 }
1169
1170 impl From<#api_struct_name> for #response_name {
1171 fn from(model: #api_struct_name) -> Self {
1172 Self {
1173 #(#response_from_assignments),*
1174 }
1175 }
1176 }
1177 };
1178
1179 (list_model, response_model)
1180}
1181
1182#[proc_macro_derive(EntityToModels, attributes(crudcrate))]
1189pub fn entity_to_models(input: TokenStream) -> TokenStream {
1190 let input = parse_macro_input!(input as DeriveInput);
1191 let struct_name = &input.ident;
1192
1193 let (table_name, api_struct_name, active_model_path, crud_meta) =
1195 match parse_and_validate_entity_attributes(&input, struct_name) {
1196 Ok(result) => result,
1197 Err(e) => return e,
1198 };
1199
1200 let fields = match extract_entity_fields(&input) {
1202 Ok(f) => f,
1203 Err(e) => return e,
1204 };
1205 let field_analysis = analyze_entity_fields(fields);
1206 if let Err(e) = validate_field_analysis(&field_analysis) {
1207 return e;
1208 }
1209
1210 let _join_validation = match setup_join_validation(&field_analysis, &api_struct_name) {
1212 Ok(validation) => validation,
1213 Err(e) => return e,
1214 };
1215
1216 let (api_struct, from_impl, crud_impl) = generate_core_api_models(
1218 struct_name,
1219 &api_struct_name,
1220 &crud_meta,
1221 &active_model_path,
1222 &field_analysis,
1223 &table_name,
1224 );
1225
1226 let (list_model, response_model) = generate_list_and_response_models(
1228 &input,
1229 &api_struct_name,
1230 struct_name,
1231 &field_analysis,
1232 );
1233
1234 let expanded = quote! {
1236 #api_struct
1237 #from_impl
1238 #crud_impl
1239 #list_model
1240 #response_model
1241 #_join_validation
1242 };
1243
1244 TokenStream::from(expanded)
1245}