1use 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
13pub struct CodeGenerator {
15 registry: SchemaRegistry,
16 output_dir: PathBuf,
17}
18
19impl CodeGenerator {
20 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 pub fn generate_all(&self) -> Result<()> {
30 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 let entities_code = self.generate_entity_structs()?;
40 self.write_module("entities.rs", entities_code)?;
41 println!(" ✓ entities.rs");
42
43 let enum_code = self.generate_ftm_entity_enum()?;
45 self.write_module("ftm_entity.rs", enum_code)?;
46 println!(" ✓ ftm_entity.rs");
47
48 let traits_code = self.generate_traits()?;
50 self.write_module("traits.rs", traits_code)?;
51 println!(" ✓ traits.rs");
52
53 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 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 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 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 #![allow(missing_docs)]
85
86 use serde::{Deserialize, Serialize};
87
88 #[cfg(feature = "builder")] use bon::Builder;
89
90 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 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 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 let mut fields = Vec::new();
135
136 fields.push(quote! {
138 pub id: String
139 });
140
141 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 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 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 let serde_attr = match (prop_type, is_required) {
173 ("number", true) => {
175 quote! { #[serde(deserialize_with = "deserialize_f64_vec", default)] }
176 }
177 ("number", false) => {
179 quote! { #[serde(skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_opt_f64_vec", default)] }
180 }
181 (_, true) => quote! { #[serde(default)] },
185 (_, false) => quote! { #[serde(skip_serializing_if = "Option::is_none")] },
186 };
187
188 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 let mut field_inits = vec![
215 quote! { id: id.into() },
216 quote! { schema: #schema_name_str.to_string() },
217 ];
218
219 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 match prop_type {
231 "json" => quote! { serde_json::Value::Object(serde_json::Map::new()) },
232 _ => quote! { Vec::new() },
233 }
234 } else {
235 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 #[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 pub fn schema_name() -> &'static str {
262 #schema_name_str
263 }
264
265 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 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 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 #![allow(missing_docs)]
331
332 use super::entities::*;
333 use serde::{Deserialize, Serialize};
334 use serde_json::Value;
335
336 #[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 pub fn schema(&self) -> &str {
347 match self {
348 #(#match_schema_arms),*
349 }
350 }
351
352 pub fn id(&self) -> &str {
354 match self {
355 #(#match_id_arms),*
356 }
357 }
358
359 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 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 fn generate_mod_file(&self) -> TokenStream {
440 quote! {
441 #![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 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 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 #![allow(missing_docs)]
477
478 #(#traits)*
484 })
485 }
486
487 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 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 let mut methods = Vec::new();
520
521 methods.push(quote! {
523 fn id(&self) -> &str;
525 });
526
527 methods.push(quote! {
528 fn schema(&self) -> &str;
530 });
531
532 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 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 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 #![allow(missing_docs)]
587
588 use super::entities::*;
589 use super::traits::*;
590
591 #(#impls)*
592 })
593 }
594
595 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 let parent_schemas = self.get_all_parent_schemas(&schema.name)?;
602
603 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 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 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 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 let is_required = schema.all_required.contains(prop_name);
644
645 let method_impl = if is_required {
646 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 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 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 let mut parents: Vec<String> = parents_set.into_iter().collect();
706 parents.sort(); Ok(parents)
708 }
709
710 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 fn map_property_type(&self, ftm_type: &str, is_required: bool) -> TokenStream {
739 if is_required {
740 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 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 fn property_to_field_name(&self, prop_name: &str) -> Ident {
760 let snake_case = self.to_snake_case(prop_name);
762
763 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 fn to_snake_case(&self, s: &str) -> String {
776 if s.to_uppercase() == s && s.len() <= 3 {
778 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 fn write_module(&self, filename: &str, tokens: TokenStream) -> Result<()> {
803 let path = self.output_dir.join(filename);
804
805 let content = match syn::parse2(tokens.clone()) {
809 Ok(syntax_tree) => prettyplease::unparse(&syntax_tree),
810 Err(_) => {
811 let raw = tokens.to_string();
813 fs::write(&path, &raw).context(format!("Failed to write file: {:?}", path))?;
814
815 let _result = std::process::Command::new("rustfmt").arg(&path).output();
817
818 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 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 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_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_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_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 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 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 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>>")); assert!(entities_content.contains("pub country: Option<Vec<String>>")); }
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}