Skip to main content

ftm_types/
codegen.rs

1//! Code generation for FTM schemas
2//!
3//! Generates type-safe Rust structs from FTM schema definitions.
4
5use anyhow::{Context, Result};
6use proc_macro2::{Ident, Span, TokenStream};
7use quote::quote;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11use crate::schema::{ResolvedSchema, SchemaRegistry};
12
13/// Code generator for FTM schemas
14pub struct CodeGenerator {
15    registry: SchemaRegistry,
16    output_dir: PathBuf,
17}
18
19impl CodeGenerator {
20    /// Create a new code generator
21    pub fn new(registry: SchemaRegistry, output_dir: impl AsRef<Path>) -> Self {
22        Self {
23            registry,
24            output_dir: output_dir.as_ref().to_path_buf(),
25        }
26    }
27
28    /// Generate all code files
29    pub fn generate_all(&self) -> Result<()> {
30        // Create output directory
31        fs::create_dir_all(&self.output_dir).context(format!(
32            "Failed to create output directory: {:?}",
33            self.output_dir
34        ))?;
35
36        println!("\nGenerating code...");
37
38        // Generate entity structs
39        let entities_code = self.generate_entity_structs()?;
40        self.write_module("entities.rs", entities_code)?;
41        println!("  ✓ entities.rs");
42
43        // Generate FtmEntity enum
44        let enum_code = self.generate_ftm_entity_enum()?;
45        self.write_module("ftm_entity.rs", enum_code)?;
46        println!("  ✓ ftm_entity.rs");
47
48        // Generate traits for abstract schemas
49        let traits_code = self.generate_traits()?;
50        self.write_module("traits.rs", traits_code)?;
51        println!("  ✓ traits.rs");
52
53        // Generate trait implementations
54        let trait_impls_code = self.generate_trait_implementations()?;
55        self.write_module("trait_impls.rs", trait_impls_code)?;
56        println!("  ✓ trait_impls.rs");
57
58        // Generate mod.rs
59        let mod_code = self.generate_mod_file();
60        self.write_module("mod.rs", mod_code)?;
61        println!("  ✓ mod.rs");
62
63        Ok(())
64    }
65
66    /// Generate entity structs for all concrete schemas
67    fn generate_entity_structs(&self) -> Result<TokenStream> {
68        let mut structs = Vec::new();
69
70        for schema_name in self.registry.schema_names() {
71            let resolved = self.registry.resolve_inheritance(&schema_name)?;
72
73            // Skip abstract schemas
74            if resolved.is_abstract() {
75                continue;
76            }
77
78            let struct_code = self.generate_entity_struct(&resolved)?;
79            structs.push(struct_code);
80        }
81
82        Ok(quote! {
83            // Auto-generated - DO NOT EDIT
84            #![allow(missing_docs)]
85
86            use serde::{Deserialize, Serialize};
87
88            #[cfg(feature = "builder")] use bon::Builder;
89
90            /// Deserialize a `Vec<f64>` whose elements may arrive as JSON strings
91            /// (e.g. `["6000.00"]`) or as JSON numbers (e.g. `[6000.0]`).
92            fn deserialize_f64_vec<'de, D>(deserializer: D) -> Result<Vec<f64>, D::Error>
93            where
94                D: serde::Deserializer<'de>,
95            {
96                Vec::<serde_json::Value>::deserialize(deserializer)?
97                    .into_iter()
98                    .map(|v| match v {
99                        serde_json::Value::Number(n) => {
100                            n.as_f64().ok_or_else(|| serde::de::Error::custom("number out of f64 range"))
101                        }
102                        serde_json::Value::String(s) => {
103                            s.parse::<f64>().map_err(serde::de::Error::custom)
104                        }
105                        other => Err(serde::de::Error::custom(
106                            format!("expected number or numeric string, got {other}")
107                        )),
108                    })
109                    .collect()
110            }
111
112            /// Same as [`deserialize_f64_vec`] but wrapped in `Some`.
113            /// Used for optional number fields so the field can still be absent (`None`)
114            /// while a present value tolerates string-encoded numbers.
115            fn deserialize_opt_f64_vec<'de, D>(deserializer: D) -> Result<Option<Vec<f64>>, D::Error>
116            where
117                D: serde::Deserializer<'de>,
118            {
119                deserialize_f64_vec(deserializer).map(Some)
120            }
121
122            #(#structs)*
123        })
124    }
125
126    /// Generate a single entity struct
127    fn generate_entity_struct(&self, schema: &ResolvedSchema) -> Result<TokenStream> {
128        let struct_name = Ident::new(&schema.name, Span::call_site());
129        let label = schema.label().unwrap_or(&schema.name);
130        let doc_comment = format!("FTM Schema: {}", label);
131        let schema_name_str = &schema.name;
132
133        // Generate fields
134        let mut fields = Vec::new();
135
136        // Add id field (required for all entities)
137        fields.push(quote! {
138            pub id: String
139        });
140
141        // Add schema field (always the schema name)
142        // For builder: automatically set to the schema name
143        // Use LitStr to create a proper string literal token
144        let schema_lit = proc_macro2::Literal::string(schema_name_str);
145        fields.push(quote! {
146            #[cfg_attr(feature = "builder", builder(default = #schema_lit.to_string()))]
147            pub schema: String
148        });
149
150        // Add properties
151        let mut property_names: Vec<_> = schema.all_properties.keys().collect();
152        property_names.sort();
153
154        for prop_name in &property_names {
155            let property = &schema.all_properties[*prop_name];
156            let field_name = self.property_to_field_name(prop_name);
157
158            // FTM default type for untyped properties is "string"
159            let prop_type = property.type_.as_deref().unwrap_or("string");
160
161            let is_required = schema.all_required.contains(*prop_name);
162            let field_type = self.map_property_type(prop_type, is_required);
163
164            let field_doc = if let Some(label) = &property.label {
165                format!("Property: {}", label)
166            } else {
167                format!("Property: {}", prop_name)
168            };
169
170            // Required fields don't skip serializing if empty and are required in builder
171            // Note: Option<_> fields don't need builder(default) as they default to None automatically
172            let serde_attr = match (prop_type, is_required) {
173                // Required number fields: tolerate string-encoded values and absent fields.
174                ("number", true) => {
175                    quote! { #[serde(deserialize_with = "deserialize_f64_vec", default)] }
176                }
177                // Optional number fields: same tolerance; `default` makes absent → None.
178                ("number", false) => {
179                    quote! { #[serde(skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_opt_f64_vec", default)] }
180                }
181                // Required Vec<String> / Vec<date> fields: default to empty vec when absent.
182                // Real-world FTM data often omits fields that the schema marks required (e.g.
183                // `name` on Payment entities), so we tolerate the absence rather than hard-error.
184                (_, true) => quote! { #[serde(default)] },
185                (_, false) => quote! { #[serde(skip_serializing_if = "Option::is_none")] },
186            };
187
188            // Builder single-value setter: wraps a single value into Vec/Option<Vec>
189            let builder_attr = match (prop_type, is_required) {
190                ("json", _) => quote! {},
191                ("number", true) => {
192                    quote! { #[cfg_attr(feature = "builder", builder(with = |value: f64| vec![value]))] }
193                }
194                ("number", false) => {
195                    quote! { #[cfg_attr(feature = "builder", builder(with = |value: f64| vec![value]))] }
196                }
197                (_, true) => {
198                    quote! { #[cfg_attr(feature = "builder", builder(with = |value: impl Into<String>| vec![value.into()]))] }
199                }
200                (_, false) => {
201                    quote! { #[cfg_attr(feature = "builder", builder(with = |value: impl Into<String>| vec![value.into()]))] }
202                }
203            };
204
205            fields.push(quote! {
206                #[doc = #field_doc]
207                #serde_attr
208                #builder_attr
209                pub #field_name: #field_type
210            });
211        }
212
213        // Generate field initializers for new() method
214        let mut field_inits = vec![
215            quote! { id: id.into() },
216            quote! { schema: #schema_name_str.to_string() },
217        ];
218
219        // Initialize all other fields
220        for prop_name in &property_names {
221            let property = &schema.all_properties[*prop_name];
222            let field_name = self.property_to_field_name(prop_name);
223
224            let prop_type = property.type_.as_deref().unwrap_or("string");
225
226            let is_required = schema.all_required.contains(*prop_name);
227
228            let init_value = if is_required {
229                // Required fields get a default empty value
230                match prop_type {
231                    "json" => quote! { serde_json::Value::Object(serde_json::Map::new()) },
232                    _ => quote! { Vec::new() },
233                }
234            } else {
235                // Optional fields are None
236                quote! { None }
237            };
238
239            field_inits.push(quote! { #field_name: #init_value });
240        }
241
242        Ok(quote! {
243            #[doc = #doc_comment]
244            #[derive(Debug, Clone, Serialize, Deserialize)]
245            #[cfg_attr(feature = "builder", derive(Builder))]
246            #[serde(rename_all = "camelCase")]
247            pub struct #struct_name {
248                #(#fields),*
249            }
250
251            impl #struct_name {
252                /// Create a new entity with the given ID
253                #[deprecated(note = "Use the builder() method instead to ensure required fields are set")]
254                pub fn new(id: impl Into<String>) -> Self {
255                    Self {
256                        #(#field_inits),*
257                    }
258                }
259
260                /// Get the schema name
261                pub fn schema_name() -> &'static str {
262                    #schema_name_str
263                }
264
265                /// Serialize to standard FTM nested JSON format
266                ///
267                /// Produces `{"id": "...", "schema": "...", "properties": {...}}`
268                pub fn to_ftm_json(&self) -> Result<String, serde_json::Error> {
269                    let mut value = serde_json::to_value(self)?;
270                    if let Some(obj) = value.as_object_mut() {
271                        let id = obj.remove("id");
272                        let schema = obj.remove("schema");
273                        let properties = serde_json::Value::Object(std::mem::take(obj));
274                        if let Some(id) = id { obj.insert("id".into(), id); }
275                        if let Some(schema) = schema { obj.insert("schema".into(), schema); }
276                        obj.insert("properties".into(), properties);
277                    }
278                    serde_json::to_string(&value)
279                }
280            }
281        })
282    }
283
284    /// Generate the FtmEntity enum
285    fn generate_ftm_entity_enum(&self) -> Result<TokenStream> {
286        let mut variants = Vec::new();
287        let mut match_schema_arms = Vec::new();
288        let mut match_id_arms = Vec::new();
289        let mut dispatch_arms = Vec::new();
290        let mut from_impls = Vec::new();
291
292        for schema_name in self.registry.schema_names() {
293            let resolved = self.registry.resolve_inheritance(&schema_name)?;
294
295            // Skip abstract schemas
296            if resolved.is_abstract() {
297                continue;
298            }
299
300            let variant_name = Ident::new(&schema_name, Span::call_site());
301            let type_name = Ident::new(&schema_name, Span::call_site());
302
303            variants.push(quote! {
304                #variant_name(#type_name)
305            });
306
307            match_schema_arms.push(quote! {
308                FtmEntity::#variant_name(_) => #schema_name
309            });
310
311            match_id_arms.push(quote! {
312                FtmEntity::#variant_name(entity) => &entity.id
313            });
314
315            dispatch_arms.push(quote! {
316                #schema_name => Ok(FtmEntity::#variant_name(serde_json::from_value(value)?))
317            });
318
319            from_impls.push(quote! {
320                impl From<#type_name> for FtmEntity {
321                    fn from(entity: #type_name) -> Self {
322                        FtmEntity::#variant_name(entity)
323                    }
324                }
325            });
326        }
327
328        Ok(quote! {
329            // Auto-generated - DO NOT EDIT
330            #![allow(missing_docs)]
331
332            use super::entities::*;
333            use serde::{Deserialize, Serialize};
334            use serde_json::Value;
335
336            /// FTM Entity enum for runtime polymorphism
337            #[derive(Debug, Clone, Serialize, Deserialize)]
338            #[serde(untagged)]
339            #[allow(clippy::large_enum_variant)]
340            pub enum FtmEntity {
341                #(#variants),*
342            }
343
344            impl FtmEntity {
345                /// Get the schema name for this entity
346                pub fn schema(&self) -> &str {
347                    match self {
348                        #(#match_schema_arms),*
349                    }
350                }
351
352                /// Get the entity ID
353                pub fn id(&self) -> &str {
354                    match self {
355                        #(#match_id_arms),*
356                    }
357                }
358
359                /// Parse FTM entity from nested JSON format
360                ///
361                /// The standard FTM JSON format has a nested structure:
362                /// ```json
363                /// {
364                ///   "id": "...",
365                ///   "schema": "Payment",
366                ///   "properties": {
367                ///     "amount": ["100"],
368                ///     "date": ["2024-01-01"]
369                ///   }
370                /// }
371                /// ```
372                ///
373                /// This function flattens the structure to match the generated Rust types.
374                /// Dispatch is done on the `"schema"` field, so the correct variant is always
375                /// selected regardless of declaration order in the enum.
376                pub fn from_ftm_json(json_str: &str) -> Result<Self, serde_json::Error> {
377                    let mut value: Value = serde_json::from_str(json_str)?;
378
379                    if let Some(obj) = value.as_object_mut()
380                        && let Some(properties) = obj.remove("properties")
381                        && let Some(props_obj) = properties.as_object()
382                    {
383                        for (key, val) in props_obj {
384                            obj.insert(key.clone(), val.clone());
385                        }
386                    }
387
388                    let schema = value
389                        .get("schema")
390                        .and_then(|v| v.as_str())
391                        .unwrap_or("");
392
393                    match schema {
394                        #(#dispatch_arms,)*
395                        _ => Err(serde::de::Error::custom(
396                            format!("unknown FTM schema: {schema:?}")
397                        )),
398                    }
399                }
400
401                /// Serialize to standard FTM nested JSON format
402                ///
403                /// Produces `{"id": "...", "schema": "...", "properties": {...}}`
404                pub fn to_ftm_json(&self) -> Result<String, serde_json::Error> {
405                    let mut value = serde_json::to_value(self)?;
406                    if let Some(obj) = value.as_object_mut() {
407                        let id = obj.remove("id");
408                        let schema = obj.remove("schema");
409                        let properties = serde_json::Value::Object(std::mem::take(obj));
410                        if let Some(id) = id { obj.insert("id".into(), id); }
411                        if let Some(schema) = schema { obj.insert("schema".into(), schema); }
412                        obj.insert("properties".into(), properties);
413                    }
414                    serde_json::to_string(&value)
415                }
416            }
417
418            impl TryFrom<String> for FtmEntity {
419                type Error = serde_json::Error;
420
421                fn try_from(s: String) -> Result<Self, Self::Error> {
422                    Self::from_ftm_json(&s)
423                }
424            }
425
426            impl TryFrom<&str> for FtmEntity {
427                type Error = serde_json::Error;
428
429                fn try_from(s: &str) -> Result<Self, Self::Error> {
430                    Self::from_ftm_json(s)
431                }
432            }
433
434            #(#from_impls)*
435        })
436    }
437
438    /// Generate mod.rs file
439    fn generate_mod_file(&self) -> TokenStream {
440        quote! {
441            // Auto-generated - DO NOT EDIT
442            #![allow(missing_docs)]
443
444            pub mod entities;
445            pub mod ftm_entity;
446            pub mod trait_impls;
447            pub mod traits;
448
449            pub use entities::*;
450            pub use ftm_entity::FtmEntity;
451            pub use traits::*;
452        }
453    }
454
455    /// Generate trait definitions for abstract schemas
456    fn generate_traits(&self) -> Result<TokenStream> {
457        let mut traits = Vec::new();
458
459        for schema_name in self.registry.schema_names() {
460            let schema = self
461                .registry
462                .get(&schema_name)
463                .context(format!("Schema not found: {}", schema_name))?;
464
465            // Only generate traits for abstract schemas
466            if !schema.abstract_.unwrap_or(false) {
467                continue;
468            }
469
470            let trait_code = self.generate_trait(&schema_name, schema)?;
471            traits.push(trait_code);
472        }
473
474        Ok(quote! {
475            // Auto-generated - DO NOT EDIT
476            #![allow(missing_docs)]
477
478            /// Traits representing FTM schema inheritance hierarchy.
479            ///
480            /// These traits enable polymorphic code that works across entity types.
481            /// All concrete entity structs implement the traits for their parent schemas.
482
483            #(#traits)*
484        })
485    }
486
487    /// Generate a single trait definition for an abstract schema
488    fn generate_trait(
489        &self,
490        schema_name: &str,
491        schema: &crate::schema::FtmSchema,
492    ) -> Result<TokenStream> {
493        let trait_name = Ident::new(schema_name, Span::call_site());
494        let doc_comment = format!(
495            "Trait for FTM schema: {}",
496            schema.label.as_deref().unwrap_or(schema_name)
497        );
498
499        // Determine parent traits
500        let parent_traits: Vec<TokenStream> = if let Some(extends) = &schema.extends {
501            extends
502                .iter()
503                .map(|parent| {
504                    let parent_ident = Ident::new(parent, Span::call_site());
505                    quote! { #parent_ident }
506                })
507                .collect()
508        } else {
509            vec![]
510        };
511
512        let trait_bounds = if parent_traits.is_empty() {
513            quote! {}
514        } else {
515            quote! { : #(#parent_traits)+* }
516        };
517
518        // Generate trait methods for properties
519        let mut methods = Vec::new();
520
521        // Add id and schema methods (all entities have these)
522        methods.push(quote! {
523            /// Get the entity ID
524            fn id(&self) -> &str;
525        });
526
527        methods.push(quote! {
528            /// Get the schema name
529            fn schema(&self) -> &str;
530        });
531
532        // Add property accessor methods
533        let mut property_names: Vec<_> = schema.properties.keys().collect();
534        property_names.sort();
535
536        for prop_name in property_names {
537            let property = &schema.properties[prop_name];
538            let method_name = self.property_to_field_name(prop_name);
539
540            let prop_type = property.type_.as_deref().unwrap_or("string");
541
542            let return_type = match prop_type {
543                "number" => quote! { Option<&[f64]> },
544                "json" => quote! { Option<&serde_json::Value> },
545                _ => quote! { Option<&[String]> },
546            };
547
548            let method_doc = if let Some(label) = &property.label {
549                format!("Get {} property", label)
550            } else {
551                format!("Get {} property", prop_name)
552            };
553
554            methods.push(quote! {
555                #[doc = #method_doc]
556                fn #method_name(&self) -> #return_type;
557            });
558        }
559
560        Ok(quote! {
561            #[doc = #doc_comment]
562            pub trait #trait_name #trait_bounds {
563                #(#methods)*
564            }
565        })
566    }
567
568    /// Generate trait implementations for concrete schemas
569    fn generate_trait_implementations(&self) -> Result<TokenStream> {
570        let mut impls = Vec::new();
571
572        for schema_name in self.registry.schema_names() {
573            let resolved = self.registry.resolve_inheritance(&schema_name)?;
574
575            // Only generate impls for concrete schemas
576            if resolved.is_abstract() {
577                continue;
578            }
579
580            let impl_code = self.generate_trait_impls_for_entity(&resolved)?;
581            impls.extend(impl_code);
582        }
583
584        Ok(quote! {
585            // Auto-generated - DO NOT EDIT
586            #![allow(missing_docs)]
587
588            use super::entities::*;
589            use super::traits::*;
590
591            #(#impls)*
592        })
593    }
594
595    /// Generate all trait implementations for a single entity
596    fn generate_trait_impls_for_entity(&self, schema: &ResolvedSchema) -> Result<Vec<TokenStream>> {
597        let mut impls = Vec::new();
598        let struct_name = Ident::new(&schema.name, Span::call_site());
599
600        // Get all parent schemas (including transitive parents)
601        let parent_schemas = self.get_all_parent_schemas(&schema.name)?;
602
603        // Generate impl for each parent trait
604        for parent_name in parent_schemas {
605            let parent_schema = self
606                .registry
607                .get(&parent_name)
608                .context(format!("Parent schema not found: {}", parent_name))?;
609
610            // Only implement traits for abstract schemas
611            if !parent_schema.abstract_.unwrap_or(false) {
612                continue;
613            }
614
615            let trait_name = Ident::new(&parent_name, Span::call_site());
616            let mut methods = Vec::new();
617
618            // Implement id and schema methods
619            methods.push(quote! {
620                fn id(&self) -> &str {
621                    &self.id
622                }
623            });
624
625            methods.push(quote! {
626                fn schema(&self) -> &str {
627                    &self.schema
628                }
629            });
630
631            // Implement property accessor methods
632            let mut property_names: Vec<_> = parent_schema.properties.keys().collect();
633            property_names.sort();
634
635            for prop_name in property_names {
636                let property = &parent_schema.properties[prop_name];
637                let method_name = self.property_to_field_name(prop_name);
638                let field_name = self.property_to_field_name(prop_name);
639
640                let prop_type = property.type_.as_deref().unwrap_or("string");
641
642                // Check if this property is required in the concrete schema
643                let is_required = schema.all_required.contains(prop_name);
644
645                let method_impl = if is_required {
646                    // Required fields return a direct reference
647                    match prop_type {
648                        "number" => quote! {
649                            fn #method_name(&self) -> Option<&[f64]> {
650                                Some(&self.#field_name)
651                            }
652                        },
653                        "json" => quote! {
654                            fn #method_name(&self) -> Option<&serde_json::Value> {
655                                Some(&self.#field_name)
656                            }
657                        },
658                        _ => quote! {
659                            fn #method_name(&self) -> Option<&[String]> {
660                                Some(&self.#field_name)
661                            }
662                        },
663                    }
664                } else {
665                    // Optional fields use as_deref/as_ref
666                    match prop_type {
667                        "number" => quote! {
668                            fn #method_name(&self) -> Option<&[f64]> {
669                                self.#field_name.as_deref()
670                            }
671                        },
672                        "json" => quote! {
673                            fn #method_name(&self) -> Option<&serde_json::Value> {
674                                self.#field_name.as_ref()
675                            }
676                        },
677                        _ => quote! {
678                            fn #method_name(&self) -> Option<&[String]> {
679                                self.#field_name.as_deref()
680                            }
681                        },
682                    }
683                };
684
685                methods.push(method_impl);
686            }
687
688            impls.push(quote! {
689                impl #trait_name for #struct_name {
690                    #(#methods)*
691                }
692            });
693        }
694
695        Ok(impls)
696    }
697
698    /// Get all parent schemas (including transitive parents) for a given schema
699    fn get_all_parent_schemas(&self, schema_name: &str) -> Result<Vec<String>> {
700        let mut parents_set = std::collections::HashSet::new();
701        let mut visited = std::collections::HashSet::new();
702        self.collect_parents_recursive(schema_name, &mut parents_set, &mut visited)?;
703
704        // Convert to Vec for iteration
705        let mut parents: Vec<String> = parents_set.into_iter().collect();
706        parents.sort(); // Sort for consistent output
707        Ok(parents)
708    }
709
710    /// Recursively collect parent schemas
711    fn collect_parents_recursive(
712        &self,
713        schema_name: &str,
714        parents: &mut std::collections::HashSet<String>,
715        visited: &mut std::collections::HashSet<String>,
716    ) -> Result<()> {
717        if visited.contains(schema_name) {
718            return Ok(());
719        }
720        visited.insert(schema_name.to_string());
721
722        let schema = self
723            .registry
724            .get(schema_name)
725            .context(format!("Schema not found: {}", schema_name))?;
726
727        if let Some(extends) = &schema.extends {
728            for parent_name in extends {
729                parents.insert(parent_name.clone());
730                self.collect_parents_recursive(parent_name, parents, visited)?;
731            }
732        }
733
734        Ok(())
735    }
736
737    /// Map FTM property types to Rust types
738    fn map_property_type(&self, ftm_type: &str, is_required: bool) -> TokenStream {
739        if is_required {
740            // Required fields are not wrapped in Option
741            match ftm_type {
742                "number" => quote! { Vec<f64> },
743                "date" => quote! { Vec<String> },
744                "json" => quote! { serde_json::Value },
745                _ => quote! { Vec<String> },
746            }
747        } else {
748            // Optional fields are wrapped in Option
749            match ftm_type {
750                "number" => quote! { Option<Vec<f64>> },
751                "date" => quote! { Option<Vec<String>> },
752                "json" => quote! { Option<serde_json::Value> },
753                _ => quote! { Option<Vec<String>> },
754            }
755        }
756    }
757
758    /// Convert property name to valid Rust field name
759    fn property_to_field_name(&self, prop_name: &str) -> Ident {
760        // Convert camelCase/PascalCase to snake_case
761        let snake_case = self.to_snake_case(prop_name);
762
763        // Handle Rust keywords
764        let field_name = match snake_case.as_str() {
765            "type" => "type_".to_string(),
766            "match" => "match_".to_string(),
767            "ref" => "ref_".to_string(),
768            _ => snake_case,
769        };
770
771        Ident::new(&field_name, Span::call_site())
772    }
773
774    /// Convert string to snake_case
775    fn to_snake_case(&self, s: &str) -> String {
776        // Handle special cases
777        if s.to_uppercase() == s && s.len() <= 3 {
778            // Acronyms like "ID", "API", etc. -> all lowercase
779            return s.to_lowercase();
780        }
781
782        let mut result = String::new();
783        let mut prev_is_upper = false;
784
785        for (i, ch) in s.chars().enumerate() {
786            if ch.is_uppercase() {
787                if i > 0 && !prev_is_upper {
788                    result.push('_');
789                }
790                result.push(ch.to_lowercase().next().unwrap());
791                prev_is_upper = true;
792            } else {
793                result.push(ch);
794                prev_is_upper = false;
795            }
796        }
797
798        result
799    }
800
801    /// Write module to file with formatting
802    fn write_module(&self, filename: &str, tokens: TokenStream) -> Result<()> {
803        let path = self.output_dir.join(filename);
804
805        // Parse and format the generated code
806        // If parsing fails (e.g., due to attributes syn doesn't recognize),
807        // write the raw tokens and let rustfmt handle it later
808        let content = match syn::parse2(tokens.clone()) {
809            Ok(syntax_tree) => prettyplease::unparse(&syntax_tree),
810            Err(_) => {
811                // Fallback: write raw tokens and format with rustfmt
812                let raw = tokens.to_string();
813                fs::write(&path, &raw).context(format!("Failed to write file: {:?}", path))?;
814
815                // Try to format with rustfmt
816                let _result = std::process::Command::new("rustfmt").arg(&path).output();
817
818                // Read back the formatted content
819                return fs::read_to_string(&path)
820                    .context("Failed to read formatted file")
821                    .map(|_| ());
822            }
823        };
824
825        fs::write(&path, content).context(format!("Failed to write file: {:?}", path))?;
826
827        // Format with rustfmt to match project style
828        let _result = std::process::Command::new("rustfmt").arg(&path).output();
829
830        Ok(())
831    }
832}
833
834#[cfg(test)]
835mod tests {
836    use super::*;
837    use crate::{generated::Person, schema::SchemaRegistry};
838    use std::io::Write;
839    use tempfile::TempDir;
840
841    fn create_test_schema(dir: &std::path::Path, name: &str, yaml: &str) {
842        let path = dir.join(format!("{}.yml", name));
843        let mut file = fs::File::create(path).unwrap();
844        file.write_all(yaml.as_bytes()).unwrap();
845    }
846
847    #[test]
848    fn test_code_generation() {
849        let temp_dir = TempDir::new().unwrap();
850
851        create_test_schema(
852            temp_dir.path(),
853            "Thing",
854            r#"
855label: Thing
856abstract: true
857properties:
858  name:
859    label: Name
860    type: name
861"#,
862        );
863
864        create_test_schema(
865            temp_dir.path(),
866            "Person",
867            r#"
868label: Person
869extends:
870  - Thing
871properties:
872  firstName:
873    label: First Name
874    type: name
875"#,
876        );
877
878        let registry = SchemaRegistry::load_from_cache(temp_dir.path()).unwrap();
879        let output_dir = temp_dir.path().join("generated");
880        let codegen = CodeGenerator::new(registry, &output_dir);
881
882        let result = codegen.generate_all();
883        assert!(result.is_ok(), "Code generation failed: {:?}", result);
884
885        // Check generated files exist
886        assert!(output_dir.join("mod.rs").exists());
887        assert!(output_dir.join("entities.rs").exists());
888        assert!(output_dir.join("ftm_entity.rs").exists());
889        assert!(output_dir.join("traits.rs").exists());
890        assert!(output_dir.join("trait_impls.rs").exists());
891    }
892
893    #[test]
894    fn test_snake_case_conversion() {
895        let temp_dir = TempDir::new().unwrap();
896
897        create_test_schema(
898            temp_dir.path(),
899            "Thing",
900            r#"
901label: Thing
902properties: {}
903"#,
904        );
905
906        let registry = SchemaRegistry::load_from_cache(temp_dir.path()).unwrap();
907        let codegen = CodeGenerator::new(registry, "/tmp/test");
908
909        assert_eq!(codegen.to_snake_case("firstName"), "first_name");
910        assert_eq!(codegen.to_snake_case("birthDate"), "birth_date");
911        assert_eq!(codegen.to_snake_case("name"), "name");
912        assert_eq!(codegen.to_snake_case("ID"), "id");
913        assert_eq!(codegen.to_snake_case("API"), "api");
914    }
915
916    #[test]
917    fn test_trait_generation() {
918        let temp_dir = TempDir::new().unwrap();
919
920        // Create abstract base schema
921        create_test_schema(
922            temp_dir.path(),
923            "Thing",
924            r#"
925label: Thing
926abstract: true
927properties:
928  name:
929    label: Name
930    type: name
931  description:
932    label: Description
933    type: text
934"#,
935        );
936
937        // Create abstract intermediate schema
938        create_test_schema(
939            temp_dir.path(),
940            "LegalEntity",
941            r#"
942label: Legal Entity
943abstract: true
944extends:
945  - Thing
946properties:
947  country:
948    label: Country
949    type: country
950"#,
951        );
952
953        // Create concrete schemas
954        create_test_schema(
955            temp_dir.path(),
956            "Person",
957            r#"
958label: Person
959extends:
960  - LegalEntity
961properties:
962  firstName:
963    label: First Name
964    type: name
965"#,
966        );
967
968        create_test_schema(
969            temp_dir.path(),
970            "Company",
971            r#"
972label: Company
973extends:
974  - LegalEntity
975properties:
976  registrationNumber:
977    label: Registration Number
978    type: identifier
979"#,
980        );
981
982        let registry = SchemaRegistry::load_from_cache(temp_dir.path()).unwrap();
983        let output_dir = temp_dir.path().join("generated");
984        let codegen = CodeGenerator::new(registry, &output_dir);
985
986        let result = codegen.generate_all();
987        assert!(result.is_ok(), "Code generation failed: {:?}", result);
988
989        // Verify traits were generated
990        let traits_content = fs::read_to_string(output_dir.join("traits.rs")).unwrap();
991        assert!(traits_content.contains("pub trait Thing"));
992        assert!(traits_content.contains("pub trait LegalEntity"));
993        assert!(traits_content.contains("fn name(&self)"));
994        assert!(traits_content.contains("fn country(&self)"));
995
996        // Verify trait implementations were generated
997        let trait_impls_content = fs::read_to_string(output_dir.join("trait_impls.rs")).unwrap();
998        assert!(trait_impls_content.contains("impl Thing for Person"));
999        assert!(trait_impls_content.contains("impl LegalEntity for Person"));
1000        assert!(trait_impls_content.contains("impl Thing for Company"));
1001        assert!(trait_impls_content.contains("impl LegalEntity for Company"));
1002
1003        // Verify concrete structs still exist with flat structure
1004        let entities_content = fs::read_to_string(output_dir.join("entities.rs")).unwrap();
1005        assert!(entities_content.contains("pub struct Person"));
1006        assert!(entities_content.contains("pub struct Company"));
1007        assert!(entities_content.contains("pub name: Option<Vec<String>>")); // Flattened from Thing
1008        assert!(entities_content.contains("pub country: Option<Vec<String>>")); // Flattened from LegalEntity
1009    }
1010
1011    #[test]
1012    fn test_builder() {
1013        let _person = Person::builder().name("Huh").height(123.45);
1014    }
1015
1016    #[test]
1017    fn test_to_ftm_json() {
1018        let person = Person::builder().name("Hello Sir").id("123".into()).build();
1019        let v: serde_json::Value = serde_json::from_str(&person.to_ftm_json().unwrap()).unwrap();
1020        let v = v.as_object().unwrap();
1021        let keys: Vec<_> = Vec::from_iter(v.keys());
1022        assert_eq!(keys, vec!["id", "properties", "schema"]);
1023    }
1024
1025    #[test]
1026    fn test_builder_single_value_setter() {
1027        let person = Person::builder()
1028            .name("John Doe")
1029            .id("123".to_string())
1030            .build();
1031        assert_eq!(person.name, vec!["John Doe".to_string()]);
1032    }
1033
1034    #[test]
1035    fn test_builder_mutate_after_build() {
1036        let mut person = Person::builder()
1037            .name("John Doe")
1038            .id("123".to_string())
1039            .build();
1040        person.name.push("Johnny".into());
1041        assert_eq!(
1042            person.name,
1043            vec!["John Doe".to_string(), "Johnny".to_string()]
1044        );
1045    }
1046}