1pub mod initial_fhir_model;
35
36use crate::initial_fhir_model::{Bundle, Resource};
37use helios_fhir::FhirVersion;
38use initial_fhir_model::ElementDefinition;
39use initial_fhir_model::StructureDefinition;
40use serde_json::Result;
41use std::fs::File;
42use std::io::BufReader;
43use std::io::{self, Write};
44use std::path::Path;
45use std::path::PathBuf;
46
47fn process_single_version(version: &FhirVersion, output_path: impl AsRef<Path>) -> io::Result<()> {
81 let resources_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources");
82 let version_dir = resources_dir.join(version.as_str());
83 std::fs::create_dir_all(output_path.as_ref())?;
85
86 let version_path = output_path
87 .as_ref()
88 .join(format!("{}.rs", version.as_str().to_lowercase()));
89
90 std::fs::write(
92 &version_path,
93 "// Generated documentation contains content from HL7 FHIR specifications\n// which may include HTML-like tags and bracket notations that are not actual HTML or links\n#![allow(rustdoc::broken_intra_doc_links)]\n#![allow(rustdoc::invalid_html_tags)]\n\nuse helios_fhir_macro::{FhirPath, FhirSerde};\nuse serde::{Deserialize, Serialize};\n\nuse crate::{DecimalElement, Element};\n\n",
94 )?;
95
96 let mut global_type_hierarchy = std::collections::HashMap::new();
98 let mut all_resources = Vec::new();
99 let mut all_complex_types = Vec::new();
100
101 let bundles: Vec<_> = visit_dirs(&version_dir)?
103 .into_iter()
104 .filter_map(|file_path| match parse_structure_definitions(&file_path) {
105 Ok(bundle) => Some(bundle),
106 Err(e) => {
107 eprintln!("Warning: Failed to parse {}: {}", file_path.display(), e);
108 None
109 }
110 })
111 .collect();
112
113 for bundle in &bundles {
115 if let Some((hierarchy, resources, complex_types)) = extract_bundle_info(bundle) {
116 global_type_hierarchy.extend(hierarchy);
117 all_resources.extend(resources);
118 all_complex_types.extend(complex_types);
119 }
120 }
121
122 for bundle in bundles {
124 generate_code(bundle, &version_path, false)?; }
126
127 generate_global_constructs(
129 &version_path,
130 &global_type_hierarchy,
131 &all_resources,
132 &all_complex_types,
133 )?;
134
135 Ok(())
136}
137
138pub fn process_fhir_version(
180 version: Option<FhirVersion>,
181 output_path: impl AsRef<Path>,
182) -> io::Result<()> {
183 match version {
184 None => {
185 for ver in [
187 #[cfg(feature = "R4")]
188 FhirVersion::R4,
189 #[cfg(feature = "R4B")]
190 FhirVersion::R4B,
191 #[cfg(feature = "R5")]
192 FhirVersion::R5,
193 #[cfg(feature = "R6")]
194 FhirVersion::R6,
195 ] {
196 if let Err(e) = process_single_version(&ver, &output_path) {
197 eprintln!("Warning: Failed to process {:?}: {}", ver, e);
198 }
199 }
200 Ok(())
201 }
202 Some(specific_version) => process_single_version(&specific_version, output_path),
203 }
204}
205
206fn visit_dirs(dir: &Path) -> io::Result<Vec<PathBuf>> {
231 let mut json_files = Vec::new();
232 if dir.is_dir() {
233 for entry in std::fs::read_dir(dir)? {
234 let entry = entry?;
235 let path = entry.path();
236 if path.is_dir() {
237 json_files.extend(visit_dirs(&path)?);
238 } else if let Some(ext) = path.extension() {
239 if ext == "json" {
240 if let Some(filename) = path.file_name() {
241 let filename = filename.to_string_lossy();
242 if !filename.contains("conceptmap")
243 && !filename.contains("valueset")
244 && !filename.contains("bundle-entry")
245 {
246 json_files.push(path);
247 }
248 }
249 }
250 }
251 }
252 }
253 Ok(json_files)
254}
255
256fn parse_structure_definitions<P: AsRef<Path>>(path: P) -> Result<Bundle> {
274 let file = File::open(path).map_err(serde_json::Error::io)?;
275 let reader = BufReader::new(file);
276 serde_json::from_reader(reader)
277}
278
279fn is_valid_structure_definition(def: &StructureDefinition) -> bool {
299 (def.kind == "complex-type" || def.kind == "primitive-type" || def.kind == "resource")
300 && def.derivation.as_deref() == Some("specialization")
301 && !def.r#abstract
302}
303
304fn is_primitive_type(def: &StructureDefinition) -> bool {
317 def.kind == "primitive-type"
318}
319
320type BundleInfo = (
321 std::collections::HashMap<String, String>,
322 Vec<String>,
323 Vec<String>,
324);
325
326fn extract_bundle_info(bundle: &Bundle) -> Option<BundleInfo> {
328 let mut type_hierarchy = std::collections::HashMap::new();
329 let mut resources = Vec::new();
330 let mut complex_types = Vec::new();
331
332 if let Some(entries) = bundle.entry.as_ref() {
333 for entry in entries {
334 if let Some(resource) = &entry.resource {
335 if let Resource::StructureDefinition(def) = resource {
336 if is_valid_structure_definition(def) {
337 if let Some(base_def) = &def.base_definition {
339 if let Some(parent) = base_def.split('/').next_back() {
340 type_hierarchy.insert(def.name.clone(), parent.to_string());
341 }
342 }
343
344 if def.kind == "resource" && !def.r#abstract {
345 resources.push(def.name.clone());
346 } else if def.kind == "complex-type" && !def.r#abstract {
347 complex_types.push(def.name.clone());
348 }
349 }
350 }
351 }
352 }
353 }
354
355 Some((type_hierarchy, resources, complex_types))
356}
357
358fn generate_global_constructs(
360 output_path: impl AsRef<Path>,
361 type_hierarchy: &std::collections::HashMap<String, String>,
362 all_resources: &[String],
363 all_complex_types: &[String],
364) -> io::Result<()> {
365 let mut file = std::fs::OpenOptions::new()
366 .create(true)
367 .append(true)
368 .open(output_path.as_ref())?;
369
370 if !all_resources.is_empty() {
372 let resource_enum = generate_resource_enum(all_resources.to_vec());
373 write!(file, "{}", resource_enum)?;
374
375 writeln!(
377 file,
378 "// --- From<T> Implementations for Element<T, Extension> ---"
379 )?;
380 writeln!(file, "impl From<bool> for Element<bool, Extension> {{")?;
381 writeln!(file, " fn from(value: bool) -> Self {{")?;
382 writeln!(file, " Self {{")?;
383 writeln!(file, " value: Some(value),")?;
384 writeln!(file, " ..Default::default()")?;
385 writeln!(file, " }}")?;
386 writeln!(file, " }}")?;
387 writeln!(file, "}}")?;
388
389 writeln!(
390 file,
391 "impl From<std::primitive::i32> for Element<std::primitive::i32, Extension> {{"
392 )?;
393 writeln!(file, " fn from(value: std::primitive::i32) -> Self {{")?;
394 writeln!(file, " Self {{")?;
395 writeln!(file, " value: Some(value),")?;
396 writeln!(file, " ..Default::default()")?;
397 writeln!(file, " }}")?;
398 writeln!(file, " }}")?;
399 writeln!(file, "}}")?;
400
401 writeln!(
402 file,
403 "impl From<std::string::String> for Element<std::string::String, Extension> {{"
404 )?;
405 writeln!(file, " fn from(value: std::string::String) -> Self {{")?;
406 writeln!(file, " Self {{")?;
407 writeln!(file, " value: Some(value),")?;
408 writeln!(file, " ..Default::default()")?;
409 writeln!(file, " }}")?;
410 writeln!(file, " }}")?;
411 writeln!(file, "}}")?;
412 writeln!(file, "// --- End From<T> Implementations ---")?;
413 }
414
415 if !type_hierarchy.is_empty() {
417 let type_hierarchy_module = generate_type_hierarchy_module(type_hierarchy);
418 write!(file, "{}", type_hierarchy_module)?;
419 }
420
421 if !all_complex_types.is_empty() {
423 writeln!(file, "\n// --- Complex Types Provider ---")?;
424 writeln!(file, "/// Marker struct for complex type information")?;
425 writeln!(file, "pub struct ComplexTypes;")?;
426 writeln!(
427 file,
428 "\nimpl crate::FhirComplexTypeProvider for ComplexTypes {{"
429 )?;
430 writeln!(
431 file,
432 " fn get_complex_type_names() -> Vec<&'static str> {{"
433 )?;
434 writeln!(file, " vec![")?;
435 for complex_type in all_complex_types {
436 writeln!(file, " \"{}\",", complex_type)?;
437 }
438 writeln!(file, " ]")?;
439 writeln!(file, " }}")?;
440 writeln!(file, "}}")?;
441 }
442
443 Ok(())
444}
445
446fn generate_code(
474 bundle: Bundle,
475 output_path: impl AsRef<Path>,
476 _generate_globals: bool,
477) -> io::Result<()> {
478 let mut all_elements = Vec::new();
480
481 if let Some(entries) = bundle.entry.as_ref() {
482 for entry in entries {
484 if let Some(resource) = &entry.resource {
485 if let Resource::StructureDefinition(def) = resource {
486 if is_valid_structure_definition(def) {
487 if let Some(snapshot) = &def.snapshot {
488 if let Some(elements) = &snapshot.element {
489 all_elements.extend(elements.iter());
490 }
491 }
492 }
493 }
494 }
495 }
496
497 let element_refs: Vec<&ElementDefinition> = all_elements;
499 let cycles = detect_struct_cycles(&element_refs);
500
501 for entry in entries {
503 if let Some(resource) = &entry.resource {
504 match resource {
505 Resource::StructureDefinition(def) => {
506 if is_valid_structure_definition(def) {
507 let content = structure_definition_to_rust(def, &cycles);
508 let mut file = std::fs::OpenOptions::new()
509 .create(true)
510 .append(true)
511 .open(output_path.as_ref())?;
512 write!(file, "{}", content)?;
513 }
514 }
515 Resource::SearchParameter(_param) => {
516 }
518 Resource::OperationDefinition(_op) => {
519 }
521 _ => {} }
523 }
524 }
525 }
526
527 Ok(())
528}
529
530fn generate_resource_enum(resources: Vec<String>) -> String {
550 let mut output = String::new();
551 output.push_str("#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, FhirPath)]\n");
553 output.push_str("#[serde(tag = \"resourceType\")]\n");
554 output.push_str("pub enum Resource {\n");
555
556 for resource in resources {
557 output.push_str(&format!(" {}({}),\n", resource, resource));
558 }
559
560 output.push_str("}\n\n");
561 output
562}
563
564fn generate_type_hierarchy_module(
577 type_hierarchy: &std::collections::HashMap<String, String>,
578) -> String {
579 let mut output = String::new();
580
581 output.push_str("\n// --- Type Hierarchy Module ---\n");
582 output.push_str("/// Type hierarchy information extracted from FHIR specifications\n");
583 output.push_str("pub mod type_hierarchy {\n");
584 output.push_str(" use std::collections::HashMap;\n");
585 output.push_str(" use std::sync::OnceLock;\n\n");
586
587 output.push_str(" /// Maps FHIR type names to their parent types\n");
589 output.push_str(" static TYPE_PARENTS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();\n\n");
590
591 output
592 .push_str(" fn get_type_parents() -> &'static HashMap<&'static str, &'static str> {\n");
593 output.push_str(" TYPE_PARENTS.get_or_init(|| {\n");
594 output.push_str(" let mut m = HashMap::new();\n");
595
596 let mut sorted_entries: Vec<_> = type_hierarchy.iter().collect();
598 sorted_entries.sort_by_key(|(k, _)| k.as_str());
599
600 for (child, parent) in sorted_entries {
601 output.push_str(&format!(
602 " m.insert(\"{}\", \"{}\");\n",
603 child, parent
604 ));
605 }
606
607 output.push_str(" m\n");
608 output.push_str(" })\n");
609 output.push_str(" }\n\n");
610
611 output.push_str(" /// Checks if a type is a subtype of another type\n");
613 output.push_str(" pub fn is_subtype_of(child: &str, parent: &str) -> bool {\n");
614 output.push_str(" // Direct match\n");
615 output.push_str(" if child.eq_ignore_ascii_case(parent) {\n");
616 output.push_str(" return true;\n");
617 output.push_str(" }\n\n");
618 output.push_str(" // Walk up the type hierarchy\n");
619 output.push_str(" let mut current = child;\n");
620 output.push_str(" while let Some(&parent_type) = get_type_parents().get(current) {\n");
621 output.push_str(" if parent_type.eq_ignore_ascii_case(parent) {\n");
622 output.push_str(" return true;\n");
623 output.push_str(" }\n");
624 output.push_str(" current = parent_type;\n");
625 output.push_str(" }\n");
626 output.push_str(" false\n");
627 output.push_str(" }\n\n");
628
629 output.push_str(" /// Gets the parent type of a given type\n");
630 output.push_str(" pub fn get_parent_type(type_name: &str) -> Option<&'static str> {\n");
631 output.push_str(" get_type_parents().get(type_name).copied()\n");
632 output.push_str(" }\n\n");
633
634 output.push_str(" /// Gets all subtypes of a given parent type\n");
635 output.push_str(" pub fn get_subtypes(parent: &str) -> Vec<&'static str> {\n");
636 output.push_str(" get_type_parents().iter()\n");
637 output.push_str(" .filter_map(|(child, p)| {\n");
638 output.push_str(" if p.eq_ignore_ascii_case(parent) {\n");
639 output.push_str(" Some(*child)\n");
640 output.push_str(" } else {\n");
641 output.push_str(" None\n");
642 output.push_str(" }\n");
643 output.push_str(" })\n");
644 output.push_str(" .collect()\n");
645 output.push_str(" }\n");
646
647 output.push_str("}\n\n");
648 output
649}
650
651fn make_rust_safe(input: &str) -> String {
674 let snake_case = input
675 .chars()
676 .enumerate()
677 .fold(String::new(), |mut acc, (i, c)| {
678 if i > 0 && c.is_uppercase() {
679 acc.push('_');
680 }
681 acc.push(c.to_lowercase().next().unwrap());
682 acc
683 });
684
685 match snake_case.as_str() {
686 "type" | "use" | "abstract" | "for" | "ref" | "const" | "where" => {
687 format!("r#{}", snake_case)
688 }
689 _ => snake_case,
690 }
691}
692
693fn capitalize_first_letter(s: &str) -> String {
714 let mut chars = s.chars();
715 match chars.next() {
716 None => String::new(),
717 Some(first) => first.to_uppercase().chain(chars).collect(),
718 }
719}
720
721fn escape_doc_comment(text: &str) -> String {
734 let normalized = text
736 .replace("\r\n", "\n") .replace('\r', "\n"); let mut result = String::new();
740 let mut in_code_block = false;
741
742 for line in normalized.lines() {
744 let trimmed_line = line.trim();
745
746 if trimmed_line == "```" {
748 if in_code_block {
749 result.push_str("```\n");
751 in_code_block = false;
752 } else {
753 result.push_str("```text\n");
755 in_code_block = true;
756 }
757 continue;
758 }
759
760 let processed = line
762 .replace("*/", "*\\/")
763 .replace("/*", "/\\*")
764 .replace("(aka \"privacy tags\".", "(aka \"privacy tags\").")
766 .replace("(aka \"tagged\")", "(aka 'tagged')")
767 .replace(" <=", " \\<=")
769 .replace(" >=", " \\>=")
770 .replace("(<=", "(\\<=")
771 .replace("(>=", "(\\>=");
772
773 result.push_str(&processed);
774 result.push('\n');
775 }
776
777 result = result.replace("\n\n\n", "\n\n");
779 result.trim_end().to_string()
780}
781
782fn format_doc_content(text: &str, in_list: bool) -> Vec<String> {
796 let mut output = Vec::new();
797 let mut in_list_item = false;
798
799 for line in text.split('\n') {
800 let trimmed = line.trim_start();
801
802 let is_bullet = trimmed.starts_with("* ") && !in_list;
804 let is_dash = trimmed.starts_with("- ") && !in_list;
805 let is_numbered = !in_list && {
806 if let Some(first_space) = trimmed.find(' ') {
808 let prefix = &trimmed[..first_space];
809 (prefix.ends_with(')') || prefix.ends_with('.')) &&
811 prefix.chars().next().is_some_and(|c| c.is_numeric())
812 } else {
813 false
814 }
815 };
816
817 if is_bullet || is_numbered || is_dash {
818 in_list_item = true;
819 output.push(line.to_string());
820 } else if in_list_item {
821 if line.trim().is_empty() {
823 output.push(String::new());
825 in_list_item = false;
826 } else if trimmed.starts_with("* ") || trimmed.starts_with("- ") ||
827 (trimmed.find(' ').is_some_and(|idx| {
828 let prefix = &trimmed[..idx];
829 (prefix.ends_with(')') || prefix.ends_with('.')) &&
830 prefix.chars().next().is_some_and(|c| c.is_numeric())
831 })) {
832 output.push(line.to_string());
834 } else {
835 let content = line.trim();
837 if !content.is_empty() {
838 let indent = if let Some(prev_line) = output.last() {
841 let prev_trimmed = prev_line.trim_start();
842 if let Some(space_pos) = prev_trimmed.find(' ') {
843 let prefix = &prev_trimmed[..space_pos];
844 if (prefix.ends_with(')') || prefix.ends_with('.')) &&
845 prefix.chars().next().is_some_and(|c| c.is_numeric()) {
846 " ".to_string()
848 } else {
849 " ".to_string()
850 }
851 } else {
852 " ".to_string()
853 }
854 } else {
855 " ".to_string()
856 };
857 output.push(format!("{}{}", indent, content));
858 }
859 }
860 } else {
861 output.push(line.to_string());
863 }
864 }
865
866 output
867}
868
869fn format_cardinality(min: Option<u32>, max: Option<&str>) -> String {
880 let min_val = min.unwrap_or(0);
881 let max_val = max.unwrap_or("1");
882
883 match (min_val, max_val) {
884 (0, "1") => "Optional (0..1)".to_string(),
885 (1, "1") => "Required (1..1)".to_string(),
886 (0, "*") => "Optional, Multiple (0..*)".to_string(),
887 (1, "*") => "Required, Multiple (1..*)".to_string(),
888 (min, max) => format!("{min}..{max}"),
889 }
890}
891
892fn format_constraints(constraints: &[initial_fhir_model::ElementDefinitionConstraint]) -> String {
902 if constraints.is_empty() {
903 return String::new();
904 }
905
906 let mut output = String::new();
907 output.push_str("/// ## Constraints\n");
908
909 for constraint in constraints {
910 let escaped_human = escape_doc_comment(&constraint.human);
911
912 let human_lines: Vec<&str> = escaped_human.split('\n').collect();
914
915 if human_lines.len() == 1 {
916 output.push_str(&format!("/// - **{}**: {} ({})\n",
918 constraint.key,
919 escaped_human,
920 constraint.severity
921 ));
922 } else {
923 output.push_str(&format!("/// - **{}**: {} ({})\n",
925 constraint.key,
926 human_lines[0],
927 constraint.severity
928 ));
929
930 for line in &human_lines[1..] {
932 let trimmed = line.trim();
933 if !trimmed.is_empty() {
934 output.push_str(&format!("/// {}\n", trimmed));
935 }
936 }
937 }
938
939 if let Some(expr) = &constraint.expression {
940 output.push_str(&format!("/// Expression: `{}`\n", escape_doc_comment(expr)));
941 }
942 }
943
944 output
945}
946
947fn format_examples(examples: &[initial_fhir_model::ElementDefinitionExample]) -> String {
957 if examples.is_empty() {
958 return String::new();
959 }
960
961 let mut output = String::new();
962 output.push_str("/// ## Examples\n");
963
964 for example in examples {
965 output.push_str(&format!("/// - {}: {:?}\n",
966 escape_doc_comment(&example.label),
967 example.value
968 ));
969 }
970
971 output
972}
973
974fn format_binding(binding: Option<&initial_fhir_model::ElementDefinitionBinding>) -> String {
984 if let Some(b) = binding {
985 let mut output = String::new();
986 output.push_str("/// ## Binding\n");
987
988 output.push_str(&format!("/// - **Strength**: {}\n", b.strength));
989
990 if let Some(desc) = &b.description {
991 let escaped_desc = escape_doc_comment(desc);
992 let desc_lines: Vec<&str> = escaped_desc.split('\n').collect();
993
994 if desc_lines.len() == 1 {
995 output.push_str(&format!("/// - **Description**: {}\n", escaped_desc));
997 } else {
998 output.push_str(&format!("/// - **Description**: {}\n", desc_lines[0]));
1000
1001 for line in &desc_lines[1..] {
1003 let trimmed = line.trim();
1004 if !trimmed.is_empty() {
1005 output.push_str(&format!("/// {}\n", trimmed));
1006 }
1007 }
1008 }
1009 }
1010
1011 if let Some(vs) = &b.value_set {
1012 output.push_str(&format!("/// - **ValueSet**: {}\n", vs));
1013 }
1014
1015 output
1016 } else {
1017 String::new()
1018 }
1019}
1020
1021fn generate_struct_documentation(sd: &StructureDefinition) -> String {
1033 let mut output = String::new();
1034
1035 output.push_str(&format!("/// FHIR {} type\n", capitalize_first_letter(&sd.name)));
1037
1038 if let Some(desc) = &sd.description {
1040 if !desc.is_empty() {
1041 output.push_str("/// \n");
1042 let escaped_desc = escape_doc_comment(desc);
1043 let formatted_lines = format_doc_content(&escaped_desc, false);
1044
1045 for line in formatted_lines {
1047 if line.is_empty() {
1048 output.push_str("/// \n");
1049 } else if line.len() <= 77 {
1050 output.push_str("/// ");
1052 output.push_str(&line);
1053 output.push('\n');
1054 } else {
1055 let words = line.split_whitespace().collect::<Vec<_>>();
1057 let mut current_line = String::new();
1058
1059 let trimmed_line = line.trim_start();
1062 let is_list_item = trimmed_line.starts_with("* ") ||
1063 trimmed_line.starts_with("- ") ||
1064 trimmed_line.find(' ').is_some_and(|idx| {
1065 let prefix = &trimmed_line[..idx];
1066 (prefix.ends_with(')') || prefix.ends_with('.')) &&
1067 prefix.chars().next().is_some_and(|c| c.is_numeric())
1068 });
1069
1070 let is_numbered_list = trimmed_line.find(' ').is_some_and(|idx| {
1072 let prefix = &trimmed_line[..idx];
1073 (prefix.ends_with(')') || prefix.ends_with('.')) &&
1074 prefix.chars().next().is_some_and(|c| c.is_numeric())
1075 });
1076
1077 let indent = if line.starts_with(" ") {
1078 " " } else if line.starts_with(" ") {
1080 " " } else if is_numbered_list {
1082 " "
1084 } else if is_list_item {
1085 " "
1087 } else {
1088 ""
1089 };
1090
1091 let first_line_indent = if is_list_item { "" } else { indent };
1093
1094 for word in words.iter() {
1095 if current_line.is_empty() {
1096 current_line = if !first_line_indent.is_empty() {
1098 format!("{}{}", first_line_indent, word)
1099 } else {
1100 word.to_string()
1101 };
1102 } else if current_line.len() + 1 + word.len() <= 77 {
1103 current_line.push(' ');
1104 current_line.push_str(word);
1105 } else {
1106 output.push_str("/// ");
1108 output.push_str(¤t_line);
1109 output.push('\n');
1110 current_line = if !indent.is_empty() {
1112 format!("{}{}", indent, word)
1113 } else {
1114 word.to_string()
1115 };
1116 }
1117 }
1118
1119 if !current_line.is_empty() {
1121 output.push_str("/// ");
1122 output.push_str(¤t_line);
1123 output.push('\n');
1124 }
1125 }
1126 }
1127 }
1128 }
1129
1130 if let Some(purpose) = &sd.purpose {
1132 if !purpose.is_empty() {
1133 output.push_str("/// \n");
1134 output.push_str("/// ## Purpose\n");
1135 let escaped_purpose = escape_doc_comment(purpose);
1136 let formatted_lines = format_doc_content(&escaped_purpose, false);
1137
1138 for line in formatted_lines {
1139 if line.is_empty() {
1140 output.push_str("/// \n");
1141 } else {
1142 output.push_str(&format!("/// {}\n", line));
1143 }
1144 }
1145 }
1146 }
1147
1148 output.push_str("/// \n");
1150 output.push_str(&format!("/// ## Type: {} type\n", capitalize_first_letter(&sd.kind)));
1151
1152 if sd.r#abstract {
1153 output.push_str("/// Abstract type (cannot be instantiated directly)\n");
1154 }
1155
1156 if let Some(base) = &sd.base_definition {
1157 output.push_str(&format!("/// Base type: {}\n", base));
1158 }
1159
1160 output.push_str("/// \n");
1162 output.push_str(&format!("/// ## Status: {}\n", sd.status));
1163
1164 if let Some(version) = &sd.fhir_version {
1166 output.push_str(&format!("/// FHIR Version: {}\n", version));
1167 }
1168
1169 output.push_str("/// \n");
1171 output.push_str(&format!("/// See: [{}]({})\n", sd.name, sd.url));
1172
1173 output
1174}
1175
1176fn generate_element_documentation(element: &ElementDefinition) -> String {
1190 let mut output = String::new();
1191
1192
1193 if let Some(short) = &element.short {
1195 output.push_str(&format!("/// {}\n", escape_doc_comment(short)));
1196 }
1197
1198 if let Some(definition) = &element.definition {
1200 if !definition.is_empty() {
1201 output.push_str("/// \n");
1202 let escaped_definition = escape_doc_comment(definition);
1203 let formatted_lines = format_doc_content(&escaped_definition, false);
1204
1205
1206 for line in formatted_lines {
1208 if line.is_empty() {
1209 output.push_str("/// \n");
1210 } else if line.len() <= 77 {
1211 output.push_str("/// ");
1213 output.push_str(&line);
1214 output.push('\n');
1215
1216 } else {
1217 let words = line.split_whitespace().collect::<Vec<_>>();
1219 let mut current_line = String::new();
1220
1221 let trimmed_line = line.trim_start();
1224 let is_list_item = trimmed_line.starts_with("* ") ||
1225 trimmed_line.starts_with("- ") ||
1226 trimmed_line.find(' ').is_some_and(|idx| {
1227 let prefix = &trimmed_line[..idx];
1228 (prefix.ends_with(')') || prefix.ends_with('.')) &&
1229 prefix.chars().next().is_some_and(|c| c.is_numeric())
1230 });
1231
1232 let is_numbered_list = trimmed_line.find(' ').is_some_and(|idx| {
1234 let prefix = &trimmed_line[..idx];
1235 (prefix.ends_with(')') || prefix.ends_with('.')) &&
1236 prefix.chars().next().is_some_and(|c| c.is_numeric())
1237 });
1238
1239 let indent = if line.starts_with(" ") {
1240 " " } else if line.starts_with(" ") {
1242 " " } else if is_numbered_list {
1244 " "
1246 } else if is_list_item {
1247 " "
1249 } else {
1250 ""
1251 };
1252
1253 let first_line_indent = if is_list_item { "" } else { indent };
1255
1256 for word in words.iter() {
1257 if current_line.is_empty() {
1258 current_line = if !first_line_indent.is_empty() {
1260 format!("{}{}", first_line_indent, word)
1261 } else {
1262 word.to_string()
1263 };
1264 } else if current_line.len() + 1 + word.len() <= 77 {
1265 current_line.push(' ');
1266 current_line.push_str(word);
1267 } else {
1268 output.push_str("/// ");
1270 output.push_str(¤t_line);
1271 output.push('\n');
1272 current_line = if !indent.is_empty() {
1274 format!("{}{}", indent, word)
1275 } else {
1276 word.to_string()
1277 };
1278 }
1279 }
1280
1281 if !current_line.is_empty() {
1283 output.push_str("/// ");
1284 output.push_str(¤t_line);
1285 output.push('\n');
1286 }
1287 }
1288 }
1289 }
1290 }
1291
1292 if let Some(requirements) = &element.requirements {
1294 if !requirements.is_empty() {
1295 output.push_str("/// \n");
1296 output.push_str("/// ## Requirements\n");
1297 let escaped_requirements = escape_doc_comment(requirements);
1298 let formatted_lines = format_doc_content(&escaped_requirements, false);
1299
1300 for line in formatted_lines {
1301 if line.is_empty() {
1302 output.push_str("/// \n");
1303 } else if line.len() <= 77 {
1304 output.push_str("/// ");
1306 output.push_str(&line);
1307 output.push('\n');
1308 } else {
1309 let words = line.split_whitespace().collect::<Vec<_>>();
1311 let mut current_line = String::new();
1312
1313 let trimmed_line = line.trim_start();
1316 let is_list_item = trimmed_line.starts_with("* ") ||
1317 trimmed_line.starts_with("- ") ||
1318 trimmed_line.find(' ').is_some_and(|idx| {
1319 let prefix = &trimmed_line[..idx];
1320 (prefix.ends_with(')') || prefix.ends_with('.')) &&
1321 prefix.chars().next().is_some_and(|c| c.is_numeric())
1322 });
1323
1324 let is_numbered_list = trimmed_line.find(' ').is_some_and(|idx| {
1326 let prefix = &trimmed_line[..idx];
1327 (prefix.ends_with(')') || prefix.ends_with('.')) &&
1328 prefix.chars().next().is_some_and(|c| c.is_numeric())
1329 });
1330
1331 let indent = if line.starts_with(" ") {
1332 " " } else if line.starts_with(" ") {
1334 " " } else if is_numbered_list {
1336 " "
1338 } else if is_list_item {
1339 " "
1341 } else {
1342 ""
1343 };
1344
1345 let first_line_indent = if is_list_item { "" } else { indent };
1347
1348 for word in words.iter() {
1349 if current_line.is_empty() {
1350 current_line = if !first_line_indent.is_empty() {
1352 format!("{}{}", first_line_indent, word)
1353 } else {
1354 word.to_string()
1355 };
1356 } else if current_line.len() + 1 + word.len() <= 77 {
1357 current_line.push(' ');
1358 current_line.push_str(word);
1359 } else {
1360 output.push_str("/// ");
1362 output.push_str(¤t_line);
1363 output.push('\n');
1364 current_line = if !indent.is_empty() {
1366 format!("{}{}", indent, word)
1367 } else {
1368 word.to_string()
1369 };
1370 }
1371 }
1372
1373 if !current_line.is_empty() {
1375 output.push_str("/// ");
1376 output.push_str(¤t_line);
1377 output.push('\n');
1378 }
1379 }
1380 }
1381 }
1382 }
1383
1384 if let Some(comment) = &element.comment {
1386 if !comment.is_empty() {
1387 output.push_str("/// \n");
1388 output.push_str("/// ## Implementation Notes\n");
1389 let escaped_comment = escape_doc_comment(comment);
1390 let formatted_lines = format_doc_content(&escaped_comment, false);
1391
1392
1393 for line in formatted_lines {
1394 if line.is_empty() {
1395 output.push_str("/// \n");
1396 } else if line.len() <= 77 {
1397 output.push_str("/// ");
1399 output.push_str(&line);
1400 output.push('\n');
1401 } else {
1402 let words = line.split_whitespace().collect::<Vec<_>>();
1404 let mut current_line = String::new();
1405
1406 let trimmed_line = line.trim_start();
1409 let is_list_item = trimmed_line.starts_with("* ") ||
1410 trimmed_line.starts_with("- ") ||
1411 trimmed_line.find(' ').is_some_and(|idx| {
1412 let prefix = &trimmed_line[..idx];
1413 (prefix.ends_with(')') || prefix.ends_with('.')) &&
1414 prefix.chars().next().is_some_and(|c| c.is_numeric())
1415 });
1416
1417 let is_numbered_list = trimmed_line.find(' ').is_some_and(|idx| {
1419 let prefix = &trimmed_line[..idx];
1420 (prefix.ends_with(')') || prefix.ends_with('.')) &&
1421 prefix.chars().next().is_some_and(|c| c.is_numeric())
1422 });
1423
1424 let indent = if line.starts_with(" ") {
1425 " " } else if line.starts_with(" ") {
1427 " " } else if is_numbered_list {
1429 " "
1431 } else if is_list_item {
1432 " "
1434 } else {
1435 ""
1436 };
1437
1438 let first_line_indent = if is_list_item { "" } else { indent };
1440
1441 for word in words.iter() {
1442 if current_line.is_empty() {
1443 current_line = if !first_line_indent.is_empty() {
1445 format!("{}{}", first_line_indent, word)
1446 } else {
1447 word.to_string()
1448 };
1449 } else if current_line.len() + 1 + word.len() <= 77 {
1450 current_line.push(' ');
1451 current_line.push_str(word);
1452 } else {
1453 output.push_str("/// ");
1455 output.push_str(¤t_line);
1456 output.push('\n');
1457 current_line = if !indent.is_empty() {
1459 format!("{}{}", indent, word)
1460 } else {
1461 word.to_string()
1462 };
1463 }
1464 }
1465
1466 if !current_line.is_empty() {
1468 output.push_str("/// ");
1469 output.push_str(¤t_line);
1470 output.push('\n');
1471 }
1472 }
1473 }
1474 }
1475 }
1476
1477 let cardinality = format_cardinality(element.min, element.max.as_deref());
1479 output.push_str("/// \n");
1480 output.push_str(&format!("/// ## Cardinality: {}\n", cardinality));
1481
1482 let mut special_semantics = Vec::new();
1484
1485 if element.is_modifier == Some(true) {
1486 let mut modifier_text = "Modifier element".to_string();
1487 if let Some(reason) = &element.is_modifier_reason {
1488 modifier_text.push_str(&format!(" - {}", escape_doc_comment(reason)));
1489 }
1490 special_semantics.push(modifier_text);
1491 }
1492
1493 if element.is_summary == Some(true) {
1494 special_semantics.push("Included in summary".to_string());
1495 }
1496
1497 if element.must_support == Some(true) {
1498 special_semantics.push("Must be supported".to_string());
1499 }
1500
1501 if let Some(meaning) = &element.meaning_when_missing {
1502 special_semantics.push(format!("When missing: {}", escape_doc_comment(meaning)));
1503 }
1504
1505 if let Some(order) = &element.order_meaning {
1506 special_semantics.push(format!("Order meaning: {}", escape_doc_comment(order)));
1507 }
1508
1509 if !special_semantics.is_empty() {
1510 output.push_str("/// \n");
1511 output.push_str("/// ## Special Semantics\n");
1512 for semantic in special_semantics {
1513 output.push_str(&format!("/// - {}\n", semantic));
1514 }
1515 }
1516
1517 if let Some(constraints) = &element.constraint {
1519 let constraint_doc = format_constraints(constraints);
1520 if !constraint_doc.is_empty() {
1521 output.push_str("/// \n");
1522 output.push_str(&constraint_doc);
1523 }
1524 }
1525
1526 if let Some(examples) = &element.example {
1528 let example_doc = format_examples(examples);
1529 if !example_doc.is_empty() {
1530 output.push_str("/// \n");
1531 output.push_str(&example_doc);
1532 }
1533 }
1534
1535 let binding_doc = format_binding(element.binding.as_ref());
1537 if !binding_doc.is_empty() {
1538 output.push_str("/// \n");
1539 output.push_str(&binding_doc);
1540 }
1541
1542 if let Some(aliases) = &element.alias {
1544 if !aliases.is_empty() {
1545 output.push_str("/// \n");
1546 output.push_str("/// ## Aliases\n");
1547
1548 let all_aliases = aliases.join(", ");
1550 let escaped_aliases = escape_doc_comment(&all_aliases);
1551
1552 for line in escaped_aliases.split('\n') {
1554 if line.trim().is_empty() {
1555 output.push_str("/// \n");
1556 } else {
1557 output.push_str(&format!("/// {}\n", line));
1558 }
1559 }
1560 }
1561 }
1562
1563 if let Some(conditions) = &element.condition {
1565 if !conditions.is_empty() {
1566 output.push_str("/// \n");
1567 output.push_str("/// ## Conditions\n");
1568 output.push_str(&format!("/// Used when: {}\n", conditions.join(", ")));
1569 }
1570 }
1571
1572 let validated_output = output.lines()
1574 .enumerate()
1575 .map(|(i, line)| {
1576 if line.trim().is_empty() {
1577 "/// ".to_string()
1578 } else if line.starts_with("///") {
1579 line.to_string()
1580 } else {
1581 eprintln!("ERROR in generate_element_documentation for {}: Line {} missing /// prefix: {}",
1583 &element.path, i, line);
1584 format!("/// {}", line)
1585 }
1586 })
1587 .collect::<Vec<String>>()
1588 .join("\n");
1589
1590 if !validated_output.is_empty() && !validated_output.ends_with('\n') {
1591 format!("{}\n", validated_output)
1592 } else {
1593 validated_output
1594 }
1595}
1596
1597fn structure_definition_to_rust(
1617 sd: &StructureDefinition,
1618 cycles: &std::collections::HashSet<(String, String)>,
1619) -> String {
1620 let mut output = String::new();
1621
1622 if is_primitive_type(sd) {
1624 return generate_primitive_type(sd);
1625 }
1626
1627 let struct_doc = generate_struct_documentation(sd);
1629
1630 if let Some(snapshot) = &sd.snapshot {
1632 if let Some(elements) = &snapshot.element {
1633 let mut processed_types = std::collections::HashSet::new();
1634 let root_element_doc = elements.iter()
1636 .find(|e| e.path == sd.name)
1637 .map(generate_element_documentation)
1638 .unwrap_or_default();
1639
1640 process_elements(
1641 elements,
1642 &mut output,
1643 &mut processed_types,
1644 cycles,
1645 &sd.name,
1646 if !struct_doc.is_empty() { Some(&struct_doc) }
1647 else if !root_element_doc.is_empty() { Some(&root_element_doc) }
1648 else { None }
1649 );
1650 }
1651 }
1652 output
1653}
1654
1655fn generate_primitive_type(sd: &StructureDefinition) -> String {
1681 let type_name = &sd.name;
1682 let mut output = String::new();
1683
1684 let value_type = match type_name.as_str() {
1686 "boolean" => "bool",
1687 "integer" | "positiveInt" | "unsignedInt" => "std::primitive::i32",
1688 "decimal" => "std::primitive::f64",
1689 "integer64" => "std::primitive::i64",
1690 "string" => "std::string::String",
1691 "code" => "std::string::String",
1692 "base64Binary" => "std::string::String",
1693 "canonical" => "std::string::String",
1694 "id" => "std::string::String",
1695 "oid" => "std::string::String",
1696 "uri" => "std::string::String",
1697 "url" => "std::string::String",
1698 "uuid" => "std::string::String",
1699 "markdown" => "std::string::String",
1700 "xhtml" => "std::string::String",
1701 "date" => "crate::PrecisionDate",
1702 "dateTime" => "crate::PrecisionDateTime",
1703 "instant" => "crate::PrecisionInstant",
1704 "time" => "crate::PrecisionTime",
1705 _ => "std::string::String",
1706 };
1707
1708 match type_name.as_str() {
1710 "boolean" => {
1711 output.push_str("/// FHIR primitive type for boolean values (true/false)\n");
1712 }
1713 "integer" => {
1714 output.push_str("/// FHIR primitive type for whole number values\n");
1715 }
1716 "positiveInt" => {
1717 output.push_str("/// FHIR primitive type for positive whole number values (> 0)\n");
1718 }
1719 "unsignedInt" => {
1720 output.push_str("/// FHIR primitive type for non-negative whole number values (>= 0)\n");
1721 }
1722 "decimal" => {
1723 output.push_str("/// FHIR primitive type for decimal numbers with arbitrary precision\n");
1724 }
1725 "string" => {
1726 output.push_str("/// FHIR primitive type for character sequences\n");
1727 }
1728 "code" => {
1729 output.push_str("/// FHIR primitive type for coded values drawn from a defined set\n");
1730 }
1731 "uri" => {
1732 output.push_str("/// FHIR primitive type for Uniform Resource Identifiers (RFC 3986)\n");
1733 }
1734 "url" => {
1735 output.push_str("/// FHIR primitive type for Uniform Resource Locators\n");
1736 }
1737 "canonical" => {
1738 output.push_str("/// FHIR primitive type for canonical URLs that reference FHIR resources\n");
1739 }
1740 "base64Binary" => {
1741 output.push_str("/// FHIR primitive type for base64-encoded binary data\n");
1742 }
1743 "date" => {
1744 output.push_str("/// FHIR primitive type for date values (year, month, day)\n");
1745 }
1746 "dateTime" => {
1747 output.push_str("/// FHIR primitive type for date and time values\n");
1748 }
1749 "instant" => {
1750 output.push_str("/// FHIR primitive type for instant in time values (to millisecond precision)\n");
1751 }
1752 "time" => {
1753 output.push_str("/// FHIR primitive type for time of day values\n");
1754 }
1755 "id" => {
1756 output.push_str("/// FHIR primitive type for logical IDs within FHIR resources\n");
1757 }
1758 "oid" => {
1759 output.push_str("/// FHIR primitive type for Object Identifiers (OIDs)\n");
1760 }
1761 "uuid" => {
1762 output.push_str("/// FHIR primitive type for Universally Unique Identifiers (UUIDs)\n");
1763 }
1764 "markdown" => {
1765 output.push_str("/// FHIR primitive type for markdown-formatted text\n");
1766 }
1767 "xhtml" => {
1768 output.push_str("/// FHIR primitive type for XHTML-formatted text with limited subset\n");
1769 }
1770 _ => {
1771 output.push_str(&format!("/// FHIR primitive type {}\n", capitalize_first_letter(type_name)));
1772 }
1773 }
1774
1775 if let Some(desc) = &sd.description {
1777 if !desc.is_empty() {
1778 output.push_str("/// \n");
1779 output.push_str(&format!("/// {}\n", escape_doc_comment(desc)));
1780 }
1781 }
1782
1783 output.push_str(&format!("/// \n/// See: [{}]({})\n", sd.name, sd.url));
1785
1786 if type_name == "decimal" {
1788 output.push_str("pub type Decimal = DecimalElement<Extension>;\n\n");
1789 } else {
1790 output.push_str(&format!(
1791 "pub type {} = Element<{}, Extension>;\n\n",
1792 capitalize_first_letter(type_name),
1793 value_type
1794 ));
1795 }
1797
1798 output
1799}
1800
1801fn detect_struct_cycles(
1829 elements: &Vec<&ElementDefinition>,
1830) -> std::collections::HashSet<(String, String)> {
1831 let mut cycles = std::collections::HashSet::new();
1832 let mut graph: std::collections::HashMap<String, Vec<String>> =
1833 std::collections::HashMap::new();
1834
1835 for element in elements {
1837 if let Some(types) = &element.r#type {
1838 let path_parts: Vec<&str> = element.path.split('.').collect();
1839 if path_parts.len() > 1 {
1840 let from_type = path_parts[0].to_string();
1841 if !from_type.is_empty() && element.max.as_deref() == Some("1") {
1842 for ty in types {
1843 if !ty.code.contains('.') && from_type != ty.code {
1844 graph
1845 .entry(from_type.clone())
1846 .or_default()
1847 .push(ty.code.clone());
1848 }
1849 }
1850 }
1851 }
1852 }
1853 }
1854
1855 for (from_type, deps) in &graph {
1857 for to_type in deps {
1858 if let Some(back_deps) = graph.get(to_type) {
1859 if back_deps.contains(from_type) {
1860 cycles.insert((from_type.clone(), to_type.clone()));
1862 }
1863 }
1864 }
1865 }
1866
1867 if elements
1869 .iter()
1870 .any(|e| e.id.as_ref().is_some_and(|id| id == "Bundle.issues"))
1871 {
1872 cycles.insert(("Bundle".to_string(), "Resource".to_string()));
1873 }
1874
1875 cycles
1876}
1877
1878fn process_elements(
1906 elements: &[ElementDefinition],
1907 output: &mut String,
1908 processed_types: &mut std::collections::HashSet<String>,
1909 cycles: &std::collections::HashSet<(String, String)>,
1910 root_type_name: &str,
1911 root_doc: Option<&str>,
1912) {
1913 let mut element_groups: std::collections::HashMap<String, Vec<&ElementDefinition>> =
1915 std::collections::HashMap::new();
1916
1917 for element in elements {
1919 let path_parts: Vec<&str> = element.path.split('.').collect();
1920 if path_parts.len() > 1 {
1921 let parent_path = path_parts[..path_parts.len() - 1].join(".");
1922 element_groups.entry(parent_path).or_default().push(element);
1923 }
1924 }
1925
1926 for (path, group) in element_groups {
1928 let type_name = generate_type_name(&path);
1929
1930 if processed_types.contains(&type_name) {
1932 continue;
1933 }
1934
1935 processed_types.insert(type_name.clone());
1936
1937 let choice_fields: Vec<_> = group.iter().filter(|e| e.path.ends_with("[x]")).collect();
1939 for choice in choice_fields {
1940 let base_name = choice
1941 .path
1942 .rsplit('.')
1943 .next()
1944 .unwrap()
1945 .trim_end_matches("[x]");
1946
1947 let enum_name = format!(
1948 "{}{}",
1949 capitalize_first_letter(&type_name),
1950 capitalize_first_letter(base_name)
1951 );
1952
1953 if processed_types.contains(&enum_name) {
1955 continue;
1956 }
1957 processed_types.insert(enum_name.clone());
1958
1959 output.push_str(&format!(
1961 "/// Choice of types for the {}\\[x\\] field in {}\n",
1962 base_name,
1963 capitalize_first_letter(&type_name)
1964 ));
1965
1966 let enum_derives = ["Debug", "Clone", "PartialEq", "Eq", "FhirSerde", "FhirPath"];
1968 output.push_str(&format!("#[derive({})]\n", enum_derives.join(", ")));
1969
1970 output.push_str(&format!(
1972 "#[fhir_choice_element(base_name = \"{}\")]\n",
1973 base_name
1974 ));
1975
1976 output.push_str(&format!("pub enum {} {{\n", enum_name));
1978
1979 if let Some(types) = &choice.r#type {
1980 for ty in types {
1981 let type_code = capitalize_first_letter(&ty.code);
1982 let rename_value = format!("{}{}", base_name, type_code);
1983
1984 output.push_str(&format!(
1986 " /// Variant accepting the {} type.\n",
1987 type_code
1988 ));
1989 output.push_str(&format!(
1990 " #[fhir_serde(rename = \"{}\")]\n",
1991 rename_value
1992 ));
1993 output.push_str(&format!(" {}({}),\n", type_code, type_code));
1994 }
1995 }
1996 output.push_str("}\n\n");
1997 }
1998
1999 let choice_element_fields: Vec<String> = group
2001 .iter()
2002 .filter(|e| e.path.ends_with("[x]"))
2003 .filter_map(|e| e.path.rsplit('.').next())
2004 .map(|name| name.trim_end_matches("[x]").to_string())
2005 .collect();
2006
2007 if path == *root_type_name {
2009 if let Some(doc) = root_doc {
2011 output.push_str(doc);
2012 }
2013 } else {
2014 if let Some(type_element) = elements.iter().find(|e| e.path == path) {
2016 let doc = generate_element_documentation(type_element);
2017 if !doc.is_empty() {
2018 output.push_str(&doc);
2019 }
2020 } else {
2021 output.push_str(&format!("/// {} sub-type\n", capitalize_first_letter(&type_name)));
2023 }
2024 }
2025
2026 let derives = [
2028 "Debug",
2029 "Clone",
2030 "PartialEq",
2031 "Eq",
2032 "FhirSerde",
2033 "FhirPath",
2034 "Default",
2035 ];
2036 output.push_str(&format!("#[derive({})]\n", derives.join(", ")));
2037
2038 if !choice_element_fields.is_empty() {
2040 let choice_elements_str = choice_element_fields.join(",");
2041 output.push_str(&format!(
2042 "#[fhir_resource(choice_elements = \"{}\")]\n",
2043 choice_elements_str
2044 ));
2045 }
2046
2047 output.push_str(&format!(
2049 "pub struct {} {{\n",
2050 capitalize_first_letter(&type_name)
2051 ));
2052
2053 for element in &group {
2054 if let Some(field_name) = element.path.rsplit('.').next() {
2055 if !field_name.contains("[x]") {
2056 generate_element_definition(element, &type_name, output, cycles, elements);
2057 } else {
2058 generate_element_definition(element, &type_name, output, cycles, elements);
2061 }
2062 }
2063 }
2064 output.push_str("}\n\n");
2065 }
2066}
2067
2068fn generate_element_definition(
2090 element: &ElementDefinition,
2091 type_name: &str,
2092 output: &mut String,
2093 cycles: &std::collections::HashSet<(String, String)>,
2094 elements: &[ElementDefinition],
2095) {
2096 if let Some(field_name) = element.path.rsplit('.').next() {
2097 let rust_field_name = make_rust_safe(field_name);
2098
2099 let mut serde_attrs = Vec::new();
2100 if field_name != rust_field_name {
2102 if field_name.ends_with("[x]") {
2104 serde_attrs.push(format!(
2105 "rename = \"{}\"",
2106 field_name.trim_end_matches("[x]")
2107 ));
2108 } else {
2109 serde_attrs.push(format!("rename = \"{}\"", field_name));
2110 }
2111 }
2112
2113 let ty = match element.r#type.as_ref().and_then(|t| t.first()) {
2114 Some(ty) => ty,
2115 None => {
2116 if let Some(content_ref) = &element.content_reference {
2117 let ref_id = extract_content_reference_id(content_ref);
2118 if let Some(referenced_element) = elements
2119 .iter()
2120 .find(|e| e.id.as_ref().is_some_and(|id| id == ref_id))
2121 {
2122 if let Some(ref_ty) =
2123 referenced_element.r#type.as_ref().and_then(|t| t.first())
2124 {
2125 ref_ty
2126 } else {
2127 return;
2128 }
2129 } else {
2130 return;
2131 }
2132 } else {
2133 return;
2134 }
2135 }
2136 };
2137 let is_array = element.max.as_deref() == Some("*");
2138 let base_type = match ty.code.as_str() {
2139 "http://hl7.org/fhirpath/System.Boolean" => "bool",
2141 "http://hl7.org/fhirpath/System.String" => "String",
2142 "http://hl7.org/fhirpath/System.Integer" => "std::primitive::i32",
2143 "http://hl7.org/fhirpath/System.Long" => "std::primitive::i64",
2144 "http://hl7.org/fhirpath/System.Decimal" => "std::primitive::f64",
2145 "http://hl7.org/fhirpath/System.Date" => "std::string::String",
2146 "http://hl7.org/fhirpath/System.DateTime" => "std::string::String",
2147 "http://hl7.org/fhirpath/System.Time" => "std::string::String",
2148 "http://hl7.org/fhirpath/System.Quantity" => "std::string::String",
2149 "Element" | "BackboneElement" => &generate_type_name(&element.path),
2150 "Base" if element.path.contains("TestPlan") => &generate_type_name(&element.path),
2153 _ => &capitalize_first_letter(&ty.code),
2154 };
2155
2156 let base_type = if let Some(content_ref) = &element.content_reference {
2157 let ref_id = extract_content_reference_id(content_ref);
2158 if !ref_id.is_empty() {
2159 generate_type_name(ref_id)
2160 } else {
2161 base_type.to_string()
2162 }
2163 } else {
2164 base_type.to_string()
2165 };
2166
2167 let mut type_str = if field_name.ends_with("[x]") {
2168 let base_name = field_name.trim_end_matches("[x]");
2169 let enum_name = format!(
2170 "{}{}",
2171 capitalize_first_letter(type_name),
2172 capitalize_first_letter(base_name)
2173 );
2174 serde_attrs.clear(); serde_attrs.push("flatten".to_string());
2177 format!("Option<{}>", enum_name)
2178 } else if is_array {
2179 format!("Option<Vec<{}>>", base_type)
2180 } else if element.min.unwrap_or(0) == 0 {
2181 format!("Option<{}>", base_type)
2182 } else {
2183 base_type.to_string()
2184 };
2185
2186 if let Some(field_type) = element.r#type.as_ref().and_then(|t| t.first()) {
2188 let from_type = element.path.split('.').next().unwrap_or("");
2189 if !from_type.is_empty() {
2190 for (cycle_from, cycle_to) in cycles.iter() {
2191 if cycle_from == from_type && cycle_to == &field_type.code {
2192 if type_str.starts_with("Option<") {
2194 type_str = format!("Option<Box<{}>>", &type_str[7..type_str.len() - 1]);
2195 } else {
2196 type_str = format!("Box<{}>", type_str);
2197 }
2198 break;
2199 }
2200 }
2201 }
2202 }
2203
2204 let doc_comment = generate_element_documentation(element);
2206 if !doc_comment.is_empty() {
2207 if doc_comment.lines().any(|line| !line.trim().is_empty() && !line.starts_with("//")) {
2209 eprintln!("\n=== WARNING: Found doc comment with lines missing /// prefix ===");
2210 eprintln!("Field: {}", element.path);
2211 eprintln!("Doc comment has {} lines", doc_comment.lines().count());
2212 for (i, line) in doc_comment.lines().enumerate() {
2213 if !line.trim().is_empty() && !line.starts_with("//") {
2214 eprintln!(" Line {}: Missing prefix: {:?}", i, line);
2215 }
2216 }
2217 eprintln!("==================================================\n");
2218 }
2219
2220 for line in doc_comment.lines() {
2222 if line.trim().is_empty() {
2224 output.push_str(" /// \n");
2225 } else if line.starts_with("///") {
2226 output.push_str(&format!(" {}\n", line));
2227 } else {
2228 eprintln!("WARNING: Doc comment line without /// prefix: {}", line);
2230 output.push_str(&format!(" /// {}\n", line));
2231 }
2232 }
2233 }
2234
2235 if !serde_attrs.is_empty() {
2237 output.push_str(&format!(" #[fhir_serde({})]\n", serde_attrs.join(", ")));
2238 }
2239
2240 let clean_field_name = if rust_field_name.ends_with("[x]") {
2242 rust_field_name.trim_end_matches("[x]").to_string()
2243 } else {
2244 rust_field_name
2245 };
2246
2247 let line_length = 8 + clean_field_name.len() + 2 + type_str.len() + 1;
2250
2251 if line_length > 100 {
2252 if type_str.starts_with("Option<Vec<") && type_str.ends_with(">>") {
2254 let inner_type = &type_str[11..type_str.len() - 2];
2256 output.push_str(&format!(
2257 " pub {}: Option<\n Vec<{}>,\n >,\n",
2258 clean_field_name, inner_type
2259 ));
2260 } else if type_str.starts_with("Option<") && type_str.ends_with(">") {
2261 let inner_type = &type_str[7..type_str.len() - 1];
2263 output.push_str(&format!(
2264 " pub {}:\n Option<{}>,\n",
2265 clean_field_name, inner_type
2266 ));
2267 } else {
2268 output.push_str(&format!(
2270 " pub {}:\n {},\n",
2271 clean_field_name, type_str
2272 ));
2273 }
2274 } else {
2275 output.push_str(&format!(" pub {}: {},\n", clean_field_name, type_str));
2276 }
2277 }
2278}
2279
2280fn extract_content_reference_id(content_ref: &str) -> &str {
2299 if let Some(fragment_start) = content_ref.find('#') {
2300 let fragment = &content_ref[fragment_start + 1..];
2301 if !fragment.is_empty() { fragment } else { "" }
2302 } else {
2303 ""
2304 }
2305}
2306
2307fn generate_type_name(path: &str) -> String {
2331 let parts: Vec<&str> = path.split('.').collect();
2332 if !parts.is_empty() {
2333 let mut result = String::from(parts[0]);
2334 for part in &parts[1..] {
2335 result.push_str(
2336 &part
2337 .chars()
2338 .next()
2339 .unwrap()
2340 .to_uppercase()
2341 .chain(part.chars().skip(1))
2342 .collect::<String>(),
2343 );
2344 }
2345 result
2346 } else {
2347 String::from("Empty path provided to generate_type_name")
2348 }
2349}
2350
2351#[cfg(test)]
2352mod tests {
2353 use super::*;
2354 use initial_fhir_model::Resource;
2355 use std::path::PathBuf;
2356
2357 #[test]
2358 fn test_process_fhir_version() {
2359 let temp_dir = std::env::temp_dir().join("fhir_gen_test");
2361 std::fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
2362
2363 assert!(process_fhir_version(Some(FhirVersion::R4), &temp_dir).is_ok());
2365
2366 assert!(temp_dir.join("r4.rs").exists());
2368
2369 std::fs::remove_dir_all(&temp_dir).expect("Failed to clean up temp directory");
2371 }
2372
2373 #[test]
2374 fn test_detect_struct_cycles() {
2375 let elements = vec![
2376 ElementDefinition {
2377 path: "Identifier".to_string(),
2378 ..Default::default()
2379 },
2380 ElementDefinition {
2381 path: "Identifier.assigner".to_string(),
2382 r#type: Some(vec![initial_fhir_model::ElementDefinitionType::new(
2383 "Reference".to_string(),
2384 )]),
2385 max: Some("1".to_string()),
2386 ..Default::default()
2387 },
2388 ElementDefinition {
2389 path: "Reference".to_string(),
2390 ..Default::default()
2391 },
2392 ElementDefinition {
2393 path: "Reference.identifier".to_string(),
2394 r#type: Some(vec![initial_fhir_model::ElementDefinitionType::new(
2395 "Identifier".to_string(),
2396 )]),
2397 max: Some("1".to_string()),
2398 ..Default::default()
2399 },
2400 ElementDefinition {
2401 path: "Patient".to_string(),
2402 r#type: Some(vec![initial_fhir_model::ElementDefinitionType::new(
2403 "Resource".to_string(),
2404 )]),
2405 ..Default::default()
2406 },
2407 ElementDefinition {
2408 path: "Extension".to_string(),
2409 ..Default::default()
2410 },
2411 ElementDefinition {
2412 path: "Extension.extension".to_string(),
2413 r#type: Some(vec![initial_fhir_model::ElementDefinitionType::new(
2414 "Extension".to_string(),
2415 )]),
2416 max: Some("*".to_string()),
2417 ..Default::default()
2418 },
2419 ElementDefinition {
2420 path: "Base64Binary".to_string(),
2421 ..Default::default()
2422 },
2423 ElementDefinition {
2424 path: "Base64Binary.extension".to_string(),
2425 r#type: Some(vec![initial_fhir_model::ElementDefinitionType::new(
2426 "Extension".to_string(),
2427 )]),
2428 max: Some("*".to_string()),
2429 ..Default::default()
2430 },
2431 ];
2432
2433 let element_refs: Vec<&ElementDefinition> = elements.iter().collect();
2434 let cycles = detect_struct_cycles(&element_refs);
2435
2436 assert!(
2439 cycles.contains(&("Identifier".to_string(), "Reference".to_string()))
2440 || cycles.contains(&("Reference".to_string(), "Identifier".to_string()))
2441 );
2442
2443 assert!(!cycles.contains(&("Patient".to_string(), "Resource".to_string())));
2445 assert!(!cycles.contains(&("Resource".to_string(), "Patient".to_string())));
2446
2447 assert!(!cycles.contains(&("Extension".to_string(), "Extension".to_string())));
2449
2450 assert!(!cycles.contains(&("Base64Binary".to_string(), "Extension".to_string())));
2452 }
2453
2454 #[test]
2455 fn test_parse_structure_definitions() {
2456 let resources_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources");
2457 let json_files = visit_dirs(&resources_dir).expect("Failed to read resource directory");
2458 assert!(
2459 !json_files.is_empty(),
2460 "No JSON files found in resources directory"
2461 );
2462
2463 for file_path in json_files {
2464 match parse_structure_definitions(&file_path) {
2465 Ok(bundle) => {
2466 if bundle.entry.is_none() {
2468 println!(
2469 "Warning: Bundle entry is None for file: {}",
2470 file_path.display()
2471 );
2472 continue;
2473 }
2474
2475 assert!(
2477 bundle.entry.unwrap().iter().any(|e| {
2478 if let Some(resource) = &e.resource {
2479 matches!(
2480 resource,
2481 Resource::StructureDefinition(_)
2482 | Resource::SearchParameter(_)
2483 | Resource::OperationDefinition(_)
2484 )
2485 } else {
2486 false
2487 }
2488 }),
2489 "No expected resource types found in file: {}",
2490 file_path.display()
2491 );
2492 }
2493 Err(e) => {
2494 panic!("Failed to parse bundle {}: {:?}", file_path.display(), e);
2495 }
2496 }
2497 }
2498 }
2499}