1mod attribute_parser;
2mod attributes;
3mod codegen;
4mod field_analyzer;
5mod macro_implementation;
6mod relation_validator;
7mod traits;
8
9use crate::codegen::join_strategies::get_join_config;
10use heck::ToPascalCase;
11use proc_macro::TokenStream;
12use quote::{ToTokens, format_ident, quote};
13use syn::{
14 Data, DeriveInput, Fields, Lit, Meta, parse::Parser, parse_macro_input, punctuated::Punctuated,
15 token::Comma,
16};
17use traits::crudresource::structs::{CRUDResourceMeta, EntityFieldAnalysis};
18
19fn extract_active_model_type(input: &DeriveInput, name: &syn::Ident) -> proc_macro2::TokenStream {
20 let mut active_model_override = None;
21 for attr in &input.attrs {
22 if attr.path().is_ident("active_model")
23 && let Some(s) = attribute_parser::get_string_from_attr(attr)
24 {
25 active_model_override =
26 Some(syn::parse_str::<syn::Type>(&s).expect("Invalid active_model type"));
27 }
28 }
29 if let Some(ty) = active_model_override {
30 quote! { #ty }
31 } else {
32 let ident = format_ident!("{}ActiveModel", name);
33 quote! { #ident }
34 }
35}
36
37fn extract_named_fields(
38 input: &DeriveInput,
39) -> syn::punctuated::Punctuated<syn::Field, syn::token::Comma> {
40 if let Data::Struct(data) = &input.data {
41 if let Fields::Named(named) = &data.fields {
42 named.named.clone()
43 } else {
44 panic!("ToCreateModel only supports structs with named fields");
45 }
46 } else {
47 panic!("ToCreateModel can only be derived for structs");
48 }
49}
50
51fn generate_update_merge_code(
52 fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
53 included_fields: &[&syn::Field],
54) -> (Vec<proc_macro2::TokenStream>, Vec<proc_macro2::TokenStream>) {
55 let included_merge = generate_included_merge_code(included_fields);
56 let excluded_merge = generate_excluded_merge_code(fields);
57 (included_merge, excluded_merge)
58}
59
60fn generate_included_merge_code(included_fields: &[&syn::Field]) -> Vec<proc_macro2::TokenStream> {
61 included_fields
62 .iter()
63 .filter(|field| {
64 !attribute_parser::get_crudcrate_bool(field, "non_db_attr").unwrap_or(false)
65 })
66 .map(|field| {
67 let ident = &field.ident;
68 let is_optional = field_analyzer::field_is_optional(field);
69
70 if is_optional {
71 quote! {
72 model.#ident = match self.#ident {
73 Some(Some(value)) => sea_orm::ActiveValue::Set(Some(value.into())),
74 Some(None) => sea_orm::ActiveValue::Set(None),
75 None => sea_orm::ActiveValue::NotSet,
76 };
77 }
78 } else {
79 quote! {
80 model.#ident = match self.#ident {
81 Some(Some(value)) => sea_orm::ActiveValue::Set(value.into()),
82 Some(None) => {
83 return Err(sea_orm::DbErr::Custom(format!(
84 "Field '{}' is required and cannot be set to null",
85 stringify!(#ident)
86 )));
87 },
88 None => sea_orm::ActiveValue::NotSet,
89 };
90 }
91 }
92 })
93 .collect()
94}
95
96fn generate_excluded_merge_code(
97 fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
98) -> Vec<proc_macro2::TokenStream> {
99 fields
100 .iter()
101 .filter(|field| {
102 attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false)
103 && !attribute_parser::get_crudcrate_bool(field, "non_db_attr").unwrap_or(false)
104 })
105 .filter_map(|field| {
106 if let Some(expr) = attribute_parser::get_crudcrate_expr(field, "on_update") {
107 let ident = &field.ident;
108 if field_analyzer::field_is_optional(field) {
109 Some(quote! {
110 model.#ident = sea_orm::ActiveValue::Set(Some((#expr).into()));
111 })
112 } else {
113 Some(quote! {
114 model.#ident = sea_orm::ActiveValue::Set((#expr).into());
115 })
116 }
117 } else {
118 None
119 }
120 })
121 .collect()
122}
123
124fn extract_entity_fields(
125 input: &DeriveInput,
126) -> Result<&syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, TokenStream> {
127 match &input.data {
128 Data::Struct(data) => match &data.fields {
129 Fields::Named(fields) => Ok(&fields.named),
130 _ => Err(syn::Error::new_spanned(
131 input,
132 "EntityToModels only supports structs with named fields",
133 )
134 .to_compile_error()
135 .into()),
136 },
137 _ => Err(
138 syn::Error::new_spanned(input, "EntityToModels only supports structs")
139 .to_compile_error()
140 .into(),
141 ),
142 }
143}
144
145fn parse_entity_attributes(input: &DeriveInput, struct_name: &syn::Ident) -> (syn::Ident, String) {
146 let mut api_struct_name = None;
147 let mut active_model_path = None;
148
149 for attr in &input.attrs {
150 if attr.path().is_ident("crudcrate")
151 && let Meta::List(meta_list) = &attr.meta
152 && let Ok(metas) =
153 Punctuated::<Meta, Comma>::parse_terminated.parse2(meta_list.tokens.clone())
154 {
155 for meta in &metas {
156 if let Meta::NameValue(nv) = meta {
157 if nv.path.is_ident("api_struct") {
158 if let syn::Expr::Lit(expr_lit) = &nv.value
159 && let Lit::Str(s) = &expr_lit.lit
160 {
161 api_struct_name = Some(format_ident!("{}", s.value()));
162 }
163 } else if nv.path.is_ident("active_model")
164 && let syn::Expr::Lit(expr_lit) = &nv.value
165 && let Lit::Str(s) = &expr_lit.lit
166 {
167 active_model_path = Some(s.value());
168 }
169 }
170 }
171 }
172 }
173
174 let table_name = attribute_parser::extract_table_name(&input.attrs)
175 .unwrap_or_else(|| struct_name.to_string());
176 let api_struct_name =
177 api_struct_name.unwrap_or_else(|| format_ident!("{}", table_name.to_pascal_case()));
178 let active_model_path = active_model_path.unwrap_or_else(|| "ActiveModel".to_string());
179
180 (api_struct_name, active_model_path)
181}
182
183fn analyze_entity_fields(
184 fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
185) -> EntityFieldAnalysis<'_> {
186 let mut analysis = EntityFieldAnalysis {
187 db_fields: Vec::new(),
188 non_db_fields: Vec::new(),
189 primary_key_field: None,
190 sortable_fields: Vec::new(),
191 filterable_fields: Vec::new(),
192 fulltext_fields: Vec::new(),
193 join_on_one_fields: Vec::new(),
194 join_on_all_fields: Vec::new(),
195 };
197
198 for field in fields {
199 let is_non_db = attribute_parser::get_crudcrate_bool(field, "non_db_attr").unwrap_or(false);
200
201 if let Some(join_config) = get_join_config(field) {
203 if join_config.on_one {
204 analysis.join_on_one_fields.push(field);
205 }
206 if join_config.on_all {
207 analysis.join_on_all_fields.push(field);
208 }
209 }
211
212 if is_non_db {
213 analysis.non_db_fields.push(field);
214 } else {
215 analysis.db_fields.push(field);
216
217 if attribute_parser::field_has_crudcrate_flag(field, "primary_key") {
218 analysis.primary_key_field = Some(field);
219 }
220 if attribute_parser::field_has_crudcrate_flag(field, "sortable") {
221 analysis.sortable_fields.push(field);
222 }
223 if attribute_parser::field_has_crudcrate_flag(field, "filterable") {
224 analysis.filterable_fields.push(field);
225 }
226 if attribute_parser::field_has_crudcrate_flag(field, "fulltext") {
227 analysis.fulltext_fields.push(field);
228 }
229 }
230 }
231
232 analysis
233}
234
235fn validate_field_analysis(analysis: &EntityFieldAnalysis) -> Result<(), TokenStream> {
236 if analysis.primary_key_field.is_some()
237 && analysis
238 .db_fields
239 .iter()
240 .filter(|field| attribute_parser::field_has_crudcrate_flag(field, "primary_key"))
241 .count()
242 > 1
243 {
244 return Err(syn::Error::new_spanned(
245 analysis.primary_key_field.unwrap(),
246 "Only one field can be marked with 'primary_key' attribute",
247 )
248 .to_compile_error()
249 .into());
250 }
251
252 for field in &analysis.non_db_fields {
254 if !has_sea_orm_ignore(field) {
255 let field_name = field
256 .ident
257 .as_ref()
258 .map_or_else(|| "unknown".to_string(), std::string::ToString::to_string);
259 return Err(syn::Error::new_spanned(
260 field,
261 format!(
262 "Field '{field_name}' has #[crudcrate(non_db_attr)] but is missing #[sea_orm(ignore)].\n\
263 Non-database fields must be marked with both attributes.\n\
264 Add #[sea_orm(ignore)] above the #[crudcrate(...)] attribute."
265 ),
266 )
267 .to_compile_error()
268 .into());
269 }
270 }
271
272 Ok(())
273}
274
275fn has_sea_orm_ignore(field: &syn::Field) -> bool {
277 for attr in &field.attrs {
278 if attr.path().is_ident("sea_orm")
279 && let Meta::List(meta_list) = &attr.meta
280 && let Ok(metas) =
281 Punctuated::<Meta, Comma>::parse_terminated.parse2(meta_list.tokens.clone())
282 {
283 for meta in metas {
284 if let Meta::Path(path) = meta
285 && path.is_ident("ignore")
286 {
287 return true;
288 }
289 }
290 }
291 }
292 false
293}
294
295fn resolve_join_field_type_preserving_container(
296 field_type: &syn::Type,
297) -> proc_macro2::TokenStream {
298 quote! { #field_type }
301}
302
303fn generate_api_struct_content(
304 analysis: &EntityFieldAnalysis,
305 _api_struct_name: &syn::Ident,
306) -> (
307 Vec<proc_macro2::TokenStream>,
308 Vec<proc_macro2::TokenStream>,
309 std::collections::HashSet<String>,
310) {
311 let mut api_struct_fields = Vec::new();
312 let mut from_model_assignments = Vec::new();
313 let required_imports = std::collections::HashSet::new();
314
315 for field in &analysis.db_fields {
316 let field_name = &field.ident;
317 let field_type = &field.ty;
318
319 let api_field_attrs: Vec<_> = field
320 .attrs
321 .iter()
322 .filter(|attr| !attr.path().is_ident("sea_orm"))
323 .collect();
324
325 api_struct_fields.push(quote! {
326 #(#api_field_attrs)*
327 pub #field_name: #field_type
328 });
329
330 let assignment = if field_type
332 .to_token_stream()
333 .to_string()
334 .contains("DateTimeWithTimeZone")
335 {
336 if field_analyzer::field_is_optional(field) {
337 quote! {
338 #field_name: model.#field_name.map(|dt| dt.with_timezone(&chrono::Utc))
339 }
340 } else {
341 quote! {
342 #field_name: model.#field_name.with_timezone(&chrono::Utc)
343 }
344 }
345 } else {
346 quote! {
347 #field_name: model.#field_name
348 }
349 };
350
351 from_model_assignments.push(assignment);
352 }
353
354 for field in &analysis.non_db_fields {
355 let field_name = &field.ident;
356 let field_type = &field.ty;
357
358 let default_expr = attribute_parser::get_crudcrate_expr(field, "default")
359 .unwrap_or_else(|| syn::parse_quote!(Default::default()));
360
361 let crudcrate_attrs: Vec<_> = field
363 .attrs
364 .iter()
365 .filter(|attr| attr.path().is_ident("crudcrate"))
366 .collect();
367
368 let schema_attrs = if get_join_config(field).is_some() {
371 quote! { #[schema(no_recursion)] }
372 } else {
373 quote! {}
374 };
375
376 let final_field_type = if get_join_config(field).is_some() {
377 resolve_join_field_type_preserving_container(field_type)
378 } else {
379 quote! { #field_type }
380 };
381
382 let field_definition = quote! {
383 #schema_attrs
384 #(#crudcrate_attrs)*
385 pub #field_name: #final_field_type
386 };
387
388 api_struct_fields.push(field_definition);
389
390 let assignment = if get_join_config(field).is_some() {
391 let empty_value = if let Ok(syn::Type::Path(type_path)) =
392 syn::parse2::<syn::Type>(quote! { #final_field_type })
393 {
394 if let Some(segment) = type_path.path.segments.last() {
395 if segment.ident == "Vec" {
396 quote! { vec![] }
397 } else if segment.ident == "Option" {
398 quote! { None }
399 } else {
400 quote! { Default::default() }
401 }
402 } else {
403 quote! { Default::default() }
404 }
405 } else {
406 quote! { Default::default() }
407 };
408
409 quote! {
410 #field_name: #empty_value
411 }
412 } else {
413 quote! {
414 #field_name: #default_expr
415 }
416 };
417
418 from_model_assignments.push(assignment);
419 }
420
421 (api_struct_fields, from_model_assignments, required_imports)
422}
423
424fn generate_api_struct(
425 api_struct_name: &syn::Ident,
426 api_struct_fields: &[proc_macro2::TokenStream],
427 active_model_path: &str,
428 crud_meta: &crate::traits::crudresource::structs::CRUDResourceMeta,
429 analysis: &EntityFieldAnalysis,
430 _required_imports: &std::collections::HashSet<String>,
431) -> proc_macro2::TokenStream {
432 let _has_create_exclusions = analysis
434 .db_fields
435 .iter()
436 .chain(analysis.non_db_fields.iter())
437 .any(|field| attribute_parser::get_crudcrate_bool(field, "create_model") == Some(false));
438 let _has_update_exclusions = analysis
439 .db_fields
440 .iter()
441 .chain(analysis.non_db_fields.iter())
442 .any(|field| attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false));
443
444 let has_join_fields =
446 !analysis.join_on_one_fields.is_empty() || !analysis.join_on_all_fields.is_empty();
447
448 let has_fields_needing_default = has_join_fields
450 || analysis.non_db_fields.iter().any(|field| {
451 attribute_parser::get_crudcrate_bool(field, "create_model") == Some(false)
453 || attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false)
454 })
455 || analysis.db_fields.iter().any(|field| {
456 attribute_parser::get_crudcrate_bool(field, "create_model") == Some(false)
458 || attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false)
459 });
460
461 let mut derives = vec![
463 quote!(Clone),
464 quote!(Debug),
465 quote!(Serialize),
466 quote!(Deserialize),
467 quote!(ToCreateModel),
468 quote!(ToUpdateModel),
469 ];
470
471 derives.push(quote!(ToSchema));
474
475 if has_fields_needing_default && !has_join_fields {
479 derives.push(quote!(Default));
480 }
481
482 if crud_meta.derive_partial_eq {
483 derives.push(quote!(PartialEq));
484 }
485
486 if crud_meta.derive_eq {
487 derives.push(quote!(Eq));
488 }
489
490 quote! {
493 use sea_orm::ActiveValue;
494 use utoipa::ToSchema;
495 use serde::{Serialize, Deserialize};
496 use crudcrate::{ToUpdateModel, ToCreateModel};
497
498 #[derive(#(#derives),*)]
499 #[active_model = #active_model_path]
500 pub struct #api_struct_name {
501 #(#api_struct_fields),*
502 }
503 }
504}
505
506fn generate_from_impl(
507 struct_name: &syn::Ident,
508 api_struct_name: &syn::Ident,
509 from_model_assignments: &[proc_macro2::TokenStream],
510) -> proc_macro2::TokenStream {
511 quote! {
512 impl From<#struct_name> for #api_struct_name {
513 fn from(model: #struct_name) -> Self {
514 Self {
515 #(#from_model_assignments),*
516 }
517 }
518 }
519 }
520}
521
522fn generate_conditional_crud_impl(
523 api_struct_name: &syn::Ident,
524 crud_meta: &CRUDResourceMeta,
525 active_model_path: &str,
526 analysis: &EntityFieldAnalysis,
527 table_name: &str,
528) -> proc_macro2::TokenStream {
529 let has_crud_resource_fields = analysis.primary_key_field.is_some()
532 || !analysis.sortable_fields.is_empty()
533 || !analysis.filterable_fields.is_empty()
534 || !analysis.fulltext_fields.is_empty();
535
536 let crud_impl = if has_crud_resource_fields {
537 macro_implementation::generate_crud_resource_impl(
538 api_struct_name,
539 crud_meta,
540 active_model_path,
541 analysis,
542 table_name,
543 )
544 } else {
545 quote! {}
546 };
547
548 let router_impl = if crud_meta.generate_router && has_crud_resource_fields {
549 crate::codegen::router::axum::generate_router_impl(api_struct_name)
550 } else {
551 quote! {}
552 };
553
554 quote! {
555 #crud_impl
556 #router_impl
557 }
558}
559
560#[proc_macro_derive(ToCreateModel, attributes(crudcrate, active_model))]
593pub fn to_create_model(input: TokenStream) -> TokenStream {
594 let input = parse_macro_input!(input as DeriveInput);
595 let name = &input.ident;
596 let create_name = format_ident!("{}Create", name);
597
598 let active_model_type = extract_active_model_type(&input, name);
599 let fields = extract_named_fields(&input);
600 let create_struct_fields = codegen::models::create::generate_create_struct_fields(&fields);
601 let conv_lines = codegen::models::create::generate_create_conversion_lines(&fields);
602
603 let create_derives =
606 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
607
608 let expanded = quote! {
609 #[derive(#create_derives)]
610 pub struct #create_name {
611 #(#create_struct_fields),*
612 }
613
614 impl From<#create_name> for #active_model_type {
615 fn from(create: #create_name) -> Self {
616 #active_model_type {
617 #(#conv_lines),*
618 }
619 }
620 }
621 };
622
623 TokenStream::from(expanded)
624}
625
626#[proc_macro_derive(ToUpdateModel, attributes(crudcrate, active_model))]
652pub fn to_update_model(input: TokenStream) -> TokenStream {
653 let input = parse_macro_input!(input as DeriveInput);
654 let name = &input.ident;
655 let update_name = format_ident!("{}Update", name);
656
657 let active_model_type = extract_active_model_type(&input, name);
658 let fields = extract_named_fields(&input);
659 let included_fields = crate::codegen::models::update::filter_update_fields(&fields);
660 let update_struct_fields =
661 crate::codegen::models::update::generate_update_struct_fields(&included_fields);
662 let (included_merge, excluded_merge) = generate_update_merge_code(&fields, &included_fields);
663
664 let update_derives =
667 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
668
669 let expanded = quote! {
670 #[derive(#update_derives)]
671 pub struct #update_name {
672 #(#update_struct_fields),*
673 }
674
675 impl #update_name {
676 pub fn merge_fields(self, mut model: #active_model_type) -> Result<#active_model_type, sea_orm::DbErr> {
677 #(#included_merge)*
678 #(#excluded_merge)*
679 Ok(model)
680 }
681 }
682
683 impl crudcrate::traits::MergeIntoActiveModel<#active_model_type> for #update_name {
684 fn merge_into_activemodel(self, model: #active_model_type) -> Result<#active_model_type, sea_orm::DbErr> {
685 Self::merge_fields(self, model)
686 }
687 }
688 };
689
690 TokenStream::from(expanded)
691}
692
693#[proc_macro_derive(ToListModel, attributes(crudcrate))]
731pub fn to_list_model(input: TokenStream) -> TokenStream {
732 let input = parse_macro_input!(input as DeriveInput);
733 let name = &input.ident;
734 let list_name = format_ident!("{}List", name);
735
736 let fields = extract_named_fields(&input);
737 let list_struct_fields = crate::codegen::models::list::generate_list_struct_fields(&fields);
738 let list_from_assignments =
739 crate::codegen::models::list::generate_list_from_assignments(&fields);
740
741 let list_derives = quote! { Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
744
745 let expanded = quote! {
746 #[derive(#list_derives)]
747 pub struct #list_name {
748 #(#list_struct_fields),*
749 }
750
751 impl From<#name> for #list_name {
752 fn from(model: #name) -> Self {
753 Self {
754 #(#list_from_assignments),*
755 }
756 }
757 }
758 };
759
760 TokenStream::from(expanded)
761}
762
763fn parse_and_validate_entity_attributes(
926 input: &DeriveInput,
927 struct_name: &syn::Ident,
928) -> Result<(String, syn::Ident, String, CRUDResourceMeta), TokenStream> {
929 let (api_struct_name, active_model_path) = parse_entity_attributes(input, struct_name);
930 let table_name = attribute_parser::extract_table_name(&input.attrs)
931 .unwrap_or_else(|| struct_name.to_string());
932
933 let meta = attribute_parser::parse_crud_resource_meta(&input.attrs);
934 let crud_meta = meta.with_defaults(&table_name);
935
936 if syn::parse_str::<syn::Type>(&active_model_path).is_err() {
938 return Err(syn::Error::new_spanned(
939 input,
940 format!("Invalid active_model path: {active_model_path}"),
941 )
942 .to_compile_error()
943 .into());
944 }
945
946 Ok((table_name, api_struct_name, active_model_path, crud_meta))
947}
948
949fn setup_join_validation(
951 field_analysis: &EntityFieldAnalysis,
952 api_struct_name: &syn::Ident,
953) -> Result<proc_macro2::TokenStream, TokenStream> {
954 let _join_validation = relation_validator::generate_join_relation_validation(field_analysis);
958
959 let cyclic_dependency_check = relation_validator::generate_cyclic_dependency_check(
961 field_analysis,
962 &api_struct_name.to_string(),
963 );
964 if !cyclic_dependency_check.is_empty() {
965 return Err(cyclic_dependency_check.into());
966 }
967
968 Ok(quote! {})
969}
970
971fn generate_core_api_models(
973 struct_name: &syn::Ident,
974 api_struct_name: &syn::Ident,
975 crud_meta: &CRUDResourceMeta,
976 active_model_path: &str,
977 field_analysis: &EntityFieldAnalysis,
978 table_name: &str,
979) -> (
980 proc_macro2::TokenStream,
981 proc_macro2::TokenStream,
982 proc_macro2::TokenStream,
983) {
984 let (api_struct_fields, from_model_assignments, required_imports) =
985 generate_api_struct_content(field_analysis, api_struct_name);
986 let api_struct = generate_api_struct(
987 api_struct_name,
988 &api_struct_fields,
989 active_model_path,
990 crud_meta,
991 field_analysis,
992 &required_imports,
993 );
994 let from_impl = generate_from_impl(struct_name, api_struct_name, &from_model_assignments);
995 let crud_impl = generate_conditional_crud_impl(
996 api_struct_name,
997 crud_meta,
998 active_model_path,
999 field_analysis,
1000 table_name,
1001 );
1002
1003 (api_struct, from_impl, crud_impl)
1004}
1005
1006fn generate_list_and_response_models(
1008 input: &DeriveInput,
1009 api_struct_name: &syn::Ident,
1010 struct_name: &syn::Ident,
1011 field_analysis: &EntityFieldAnalysis,
1012) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
1013 let list_name = format_ident!("{}List", api_struct_name);
1015 let raw_fields = extract_named_fields(input);
1016 let list_struct_fields = crate::codegen::models::list::generate_list_struct_fields(&raw_fields);
1017 let list_from_assignments =
1018 crate::codegen::models::list::generate_list_from_assignments(&raw_fields);
1019 let list_from_model_assignments =
1020 crate::codegen::models::list::generate_list_from_model_assignments(field_analysis);
1021
1022 let list_derives =
1023 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
1024
1025 let list_model = quote! {
1026 #[derive(#list_derives)]
1027 pub struct #list_name {
1028 #(#list_struct_fields),*
1029 }
1030
1031 impl From<#api_struct_name> for #list_name {
1032 fn from(model: #api_struct_name) -> Self {
1033 Self {
1034 #(#list_from_assignments),*
1035 }
1036 }
1037 }
1038
1039 impl From<#struct_name> for #list_name {
1040 fn from(model: #struct_name) -> Self {
1041 Self {
1042 #(#list_from_model_assignments),*
1043 }
1044 }
1045 }
1046 };
1047
1048 let response_name = format_ident!("{}Response", api_struct_name);
1050 let response_struct_fields =
1051 crate::codegen::models::response::generate_response_struct_fields(&raw_fields);
1052 let response_from_assignments =
1053 crate::codegen::models::response::generate_response_from_assignments(&raw_fields);
1054
1055 let response_derives =
1056 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
1057
1058 let response_model = quote! {
1059 #[derive(#response_derives)]
1060 pub struct #response_name {
1061 #(#response_struct_fields),*
1062 }
1063
1064 impl From<#api_struct_name> for #response_name {
1065 fn from(model: #api_struct_name) -> Self {
1066 Self {
1067 #(#response_from_assignments),*
1068 }
1069 }
1070 }
1071 };
1072
1073 (list_model, response_model)
1074}
1075
1076#[proc_macro_derive(EntityToModels, attributes(crudcrate))]
1083pub fn entity_to_models(input: TokenStream) -> TokenStream {
1084 let input = parse_macro_input!(input as DeriveInput);
1085 let struct_name = &input.ident;
1086
1087 let (table_name, api_struct_name, active_model_path, crud_meta) =
1089 match parse_and_validate_entity_attributes(&input, struct_name) {
1090 Ok(result) => result,
1091 Err(e) => return e,
1092 };
1093
1094 let fields = match extract_entity_fields(&input) {
1096 Ok(f) => f,
1097 Err(e) => return e,
1098 };
1099 let field_analysis = analyze_entity_fields(fields);
1100 if let Err(e) = validate_field_analysis(&field_analysis) {
1101 return e;
1102 }
1103
1104 let _join_validation = match setup_join_validation(&field_analysis, &api_struct_name) {
1106 Ok(validation) => validation,
1107 Err(e) => return e,
1108 };
1109
1110 let (api_struct, from_impl, crud_impl) = generate_core_api_models(
1112 struct_name,
1113 &api_struct_name,
1114 &crud_meta,
1115 &active_model_path,
1116 &field_analysis,
1117 &table_name,
1118 );
1119
1120 let (list_model, response_model) =
1122 generate_list_and_response_models(&input, &api_struct_name, struct_name, &field_analysis);
1123
1124 let expanded = quote! {
1126 #api_struct
1127 #from_impl
1128 #crud_impl
1129 #list_model
1130 #response_model
1131 #_join_validation
1132 };
1133
1134 TokenStream::from(expanded)
1135}