helios_fhir_gen/
lib.rs

1//! # FHIR Code Generator
2//!
3//! This crate provides functionality to generate Rust code from FHIR StructureDefinitions.
4//! It transforms official FHIR specification JSON files into idiomatic Rust types with
5//! proper serialization/deserialization support.
6
7//!
8//! ## Overview
9//!
10//! The code generator performs the following steps:
11//! 1. Loads FHIR specification files from `resources/{VERSION}/`
12//! 2. Parses StructureDefinitions using minimal bootstrap types
13//! 3. Analyzes type hierarchies and detects circular dependencies
14//! 4. Generates strongly-typed Rust structs and enums
15//! 5. Outputs version-specific modules (e.g., `r4.rs`, `r5.rs`)
16//!
17//! ## Example Usage
18//!
19//! ```ignore
20//! use helios_fhir_gen::process_fhir_version;
21//! use helios_fhir::FhirVersion;
22//! use std::path::PathBuf;
23//!
24//! let output_dir = PathBuf::from("output");
25//!
26//! // Generate code for R4
27//! process_fhir_version(Some(FhirVersion::R4), &output_dir)?;
28//!
29//! // Generate code for all versions
30//! process_fhir_version(None, &output_dir)?;
31//! # Ok::<(), std::io::Error>(())
32//! ```
33
34pub 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
47/// Processes a single FHIR version and generates corresponding Rust code.
48///
49/// This function loads all JSON specification files for the given FHIR version,
50/// parses the StructureDefinitions, and generates Rust code for all valid types.
51///
52/// # Arguments
53///
54/// * `version` - The FHIR version to process (R4, R4B, R5, or R6)
55/// * `output_path` - Directory where the generated Rust files will be written
56///
57/// # Returns
58///
59/// Returns `Ok(())` on success, or an `io::Error` if file operations fail.
60///
61/// # Generated Output
62///
63/// Creates a single `.rs` file named after the version (e.g., `r4.rs`) containing:
64/// - Type definitions for all FHIR resources and data types
65/// - Choice type enums for polymorphic elements
66/// - A unified Resource enum for all resource types
67/// - Proper serialization/deserialization attributes
68///
69/// # Example
70///
71/// ```ignore
72/// use helios_fhir_gen::process_single_version;
73/// use helios_fhir::FhirVersion;
74/// use std::path::PathBuf;
75///
76/// let output_dir = PathBuf::from("generated");
77/// process_single_version(&FhirVersion::R4, &output_dir)?;
78/// # Ok::<(), std::io::Error>(())
79/// ```
80fn 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    // Create output directory if it doesn't exist
84    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    // Create the version-specific output file with initial content
91    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    // Collect all type hierarchy information across all bundles
97    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    // First pass: collect all bundles and extract global information
102    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    // Extract global information from all bundles
114    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    // Second pass: generate code for each bundle
123    for bundle in bundles {
124        generate_code(bundle, &version_path, false)?; // false = don't generate global constructs
125    }
126
127    // Generate global constructs once at the end
128    generate_global_constructs(
129        &version_path,
130        &global_type_hierarchy,
131        &all_resources,
132        &all_complex_types,
133    )?;
134
135    Ok(())
136}
137
138/// Processes one or more FHIR versions and generates corresponding Rust code.
139///
140/// This is the main entry point for the code generation process. It can either
141/// process a specific FHIR version or all available versions based on enabled features.
142///
143/// # Arguments
144///
145/// * `version` - Optional specific FHIR version to process. If `None`, processes all
146///   versions that are enabled via Cargo features
147/// * `output_path` - Directory where generated Rust files will be written
148///
149/// # Returns
150///
151/// Returns `Ok(())` on success. If processing multiple versions, continues even if
152/// individual versions fail (with warnings), returning `Ok(())` as long as the
153/// overall process completes.
154///
155/// # Feature Dependencies
156///
157/// The versions processed depend on which Cargo features are enabled:
158/// - `R4` - FHIR Release 4 (default)
159/// - `R4B` - FHIR Release 4B  
160/// - `R5` - FHIR Release 5
161/// - `R6` - FHIR Release 6
162///
163/// # Examples
164///
165/// ```ignore
166/// use helios_fhir_gen::process_fhir_version;
167/// use helios_fhir::FhirVersion;
168/// use std::path::PathBuf;
169///
170/// let output_dir = PathBuf::from("crates/fhir/src");
171///
172/// // Process only R4
173/// process_fhir_version(Some(FhirVersion::R4), &output_dir)?;
174///
175/// // Process all enabled versions
176/// process_fhir_version(None, &output_dir)?;
177/// # Ok::<(), std::io::Error>(())
178/// ```
179pub fn process_fhir_version(
180    version: Option<FhirVersion>,
181    output_path: impl AsRef<Path>,
182) -> io::Result<()> {
183    match version {
184        None => {
185            // Process all versions
186            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
206/// Recursively visits directories to find relevant JSON specification files.
207///
208/// This function traverses the resource directory structure and collects all JSON files
209/// that contain FHIR definitions, while filtering out files that aren't needed for
210/// code generation (like concept maps and value sets).
211///
212/// # Arguments
213///
214/// * `dir` - Root directory to search for JSON files
215///
216/// # Returns
217///
218/// Returns a vector of `PathBuf`s pointing to relevant JSON specification files,
219/// or an `io::Error` if directory traversal fails.
220///
221/// # Filtering Logic
222///
223/// Only includes JSON files that:
224/// - Have a `.json` extension
225/// - Do not contain "conceptmap" in the filename
226/// - Do not contain "valueset" in the filename
227///
228/// This filtering focuses the code generation on structural definitions rather
229/// than terminology content.
230fn 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
256/// Parses a JSON file containing FHIR StructureDefinitions into a Bundle.
257///
258/// This function reads a JSON file and deserializes it into a FHIR Bundle containing
259/// StructureDefinitions and other FHIR resources used for code generation.
260///
261/// # Arguments
262///
263/// * `path` - Path to the JSON file to parse
264///
265/// # Returns
266///
267/// Returns a `Bundle` on success, or a `serde_json::Error` if parsing fails.
268///
269/// # File Format
270///
271/// Expects JSON files in the standard FHIR Bundle format with entries containing
272/// StructureDefinition resources, as provided by the official FHIR specification.
273fn 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
279/// Determines if a StructureDefinition should be included in code generation.
280///
281/// This function filters StructureDefinitions to only include those that represent
282/// concrete types that should have Rust code generated for them.
283///
284/// # Arguments
285///
286/// * `def` - The StructureDefinition to evaluate
287///
288/// # Returns
289///
290/// Returns `true` if the StructureDefinition should be processed for code generation.
291///
292/// # Criteria
293///
294/// A StructureDefinition is considered valid if:
295/// - Kind is "complex-type", "primitive-type", or "resource"
296/// - Derivation is "specialization" (concrete implementations)
297/// - Abstract is `false` (not an abstract base type)
298fn 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
304/// Checks if a StructureDefinition represents a FHIR primitive type.
305///
306/// Primitive types are handled differently in code generation, typically being
307/// mapped to Rust primitive types or type aliases rather than full structs.
308///
309/// # Arguments
310///
311/// * `def` - The StructureDefinition to check
312///
313/// # Returns
314///
315/// Returns `true` if this is a primitive type definition.
316fn 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
326/// Extracts type hierarchy and resource information from a bundle
327fn 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                        // Extract type hierarchy from baseDefinition
338                        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
358/// Generates global constructs (Resource enum, type hierarchy, etc.) once at the end
359fn 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    // Generate the Resource enum
371    if !all_resources.is_empty() {
372        let resource_enum = generate_resource_enum(all_resources.to_vec());
373        write!(file, "{}", resource_enum)?;
374
375        // Add From<T> implementations for base types
376        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    // Generate type hierarchy module
416    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    // Generate ComplexTypes struct and FhirComplexTypeProvider implementation
422    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
446/// Generates Rust code from a Bundle of FHIR StructureDefinitions.
447///
448/// This is the main code generation function that processes all StructureDefinitions
449/// in a Bundle and writes the corresponding Rust code to a file.
450///
451/// # Arguments
452///
453/// * `bundle` - FHIR Bundle containing StructureDefinitions and other resources
454/// * `output_path` - Path to the output Rust file
455///
456/// # Returns
457///
458/// Returns `Ok(())` on success, or an `io::Error` if file operations fail.
459///
460/// # Process Overview
461///
462/// 1. **First Pass**: Collects all ElementDefinitions and detects circular dependencies
463/// 2. **Second Pass**: Generates Rust code for each valid StructureDefinition
464/// 3. **Final Step**: Generates a unified Resource enum and helper implementations
465///
466/// # Generated Code Includes
467///
468/// - Struct definitions for complex types and resources
469/// - Enum definitions for choice types (polymorphic elements)
470/// - A Resource enum containing all resource types
471/// - From<T> implementations for primitive type conversions
472/// - Proper derive macros for serialization and FHIR-specific functionality
473fn generate_code(
474    bundle: Bundle,
475    output_path: impl AsRef<Path>,
476    _generate_globals: bool,
477) -> io::Result<()> {
478    // First collect all ElementDefinitions across all StructureDefinitions
479    let mut all_elements = Vec::new();
480
481    if let Some(entries) = bundle.entry.as_ref() {
482        // First pass: collect all elements
483        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        // Detect cycles using all collected elements
498        let element_refs: Vec<&ElementDefinition> = all_elements;
499        let cycles = detect_struct_cycles(&element_refs);
500
501        // Second pass: generate code
502        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                        // TODO: Generate code for search parameter
517                    }
518                    Resource::OperationDefinition(_op) => {
519                        // TODO: Generate code for operation definition
520                    }
521                    _ => {} // Skip other resource types for now
522                }
523            }
524        }
525    }
526
527    Ok(())
528}
529
530/// Generates a Rust enum containing all FHIR resource types.
531///
532/// This function creates a single enum that can represent any FHIR resource,
533/// using serde's tag-based deserialization to automatically route JSON to
534/// the correct variant based on the "resourceType" field.
535///
536/// # Arguments
537///
538/// * `resources` - Vector of resource type names to include in the enum
539///
540/// # Returns
541///
542/// Returns a string containing the Rust enum definition.
543///
544/// # Generated Features
545///
546/// - Tagged enum with `#[serde(tag = "resourceType")]` for automatic routing
547/// - All standard derives for functionality and compatibility
548/// - Each variant contains the corresponding resource struct
549fn generate_resource_enum(resources: Vec<String>) -> String {
550    let mut output = String::new();
551    // Add Clone to the derives for the Resource enum
552    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
564/// Generates a module containing type hierarchy information extracted from FHIR specifications.
565///
566/// This function creates a module with functions to query type relationships at runtime,
567/// allowing the code to understand FHIR type inheritance without hard-coding.
568///
569/// # Arguments
570///
571/// * `type_hierarchy` - HashMap mapping type names to their parent types
572///
573/// # Returns
574///
575/// Returns a string containing the type hierarchy module definition.
576fn 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    // Generate the static HashMap
588    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    // Sort entries for consistent output
597    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    // Generate helper functions
612    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
651/// Converts a FHIR field name to a valid Rust identifier.
652///
653/// This function transforms FHIR field names into valid Rust identifiers by:
654/// - Converting camelCase to snake_case
655/// - Escaping Rust keywords with the `r#` prefix
656///
657/// # Arguments
658///
659/// * `input` - The original FHIR field name
660///
661/// # Returns
662///
663/// Returns a string that is a valid Rust identifier.
664///
665/// # Examples
666///
667/// ```ignore
668/// # use helios_fhir_gen::make_rust_safe;
669/// assert_eq!(make_rust_safe("birthDate"), "birth_date");
670/// assert_eq!(make_rust_safe("type"), "r#type");
671/// assert_eq!(make_rust_safe("abstract"), "r#abstract");
672/// ```
673fn 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
693/// Capitalizes the first letter of a string.
694///
695/// This utility function is used to convert FHIR type names to proper Rust
696/// type names that follow PascalCase conventions.
697///
698/// # Arguments
699///
700/// * `s` - The string to capitalize
701///
702/// # Returns
703///
704/// Returns a new string with the first character capitalized.
705///
706/// # Examples
707///
708/// ```ignore
709/// # use helios_fhir_gen::capitalize_first_letter;
710/// assert_eq!(capitalize_first_letter("patient"), "Patient");
711/// assert_eq!(capitalize_first_letter("humanName"), "HumanName");
712/// ```
713fn 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
721/// Escapes markdown text for use in Rust doc comments.
722///
723/// This function escapes special characters that could interfere with
724/// Rust's doc comment parsing.
725///
726/// # Arguments
727///
728/// * `text` - The markdown text to escape
729///
730/// # Returns
731///
732/// Returns the escaped text safe for use in doc comments.
733fn escape_doc_comment(text: &str) -> String {
734    // First, normalize all line endings to \n and remove bare CRs
735    let normalized = text
736        .replace("\r\n", "\n")  // Convert Windows line endings
737        .replace('\r', "\n");   // Convert bare CRs to newlines
738    
739    let mut result = String::new();
740    let mut in_code_block = false;
741    
742    // Process each line
743    for line in normalized.lines() {
744        let trimmed_line = line.trim();
745        
746        // Check for code block markers
747        if trimmed_line == "```" {
748            if in_code_block {
749                // This is a closing ```
750                result.push_str("```\n");
751                in_code_block = false;
752            } else {
753                // This is an opening ```
754                result.push_str("```text\n");
755                in_code_block = true;
756            }
757            continue;
758        }
759        
760        // Apply standard replacements
761        let processed = line
762            .replace("*/", "*\\/")
763            .replace("/*", "/\\*")
764            // Fix common typos in FHIR spec
765            .replace("(aka \"privacy tags\".", "(aka \"privacy tags\").")
766            .replace("(aka \"tagged\")", "(aka 'tagged')")
767            // Escape comparison operators that look like quote markers to clippy
768            .replace(" <=", " \\<=")
769            .replace(" >=", " \\>=")
770            .replace("(<=", "(\\<=")
771            .replace("(>=", "(\\>=");
772        
773        result.push_str(&processed);
774        result.push('\n');
775    }
776    
777    // Clean up excessive blank lines and trailing whitespace
778    result = result.replace("\n\n\n", "\n\n");
779    result.trim_end().to_string()
780}
781
782/// Formats text content for use in Rust doc comments, handling proper indentation.
783///
784/// This function ensures that multi-line content is properly formatted for Rust doc
785/// comments, including handling bullet points and numbered lists that need continuation indentation.
786///
787/// # Arguments
788///
789/// * `text` - The text to format
790/// * `in_list` - Whether we're currently in a list context
791///
792/// # Returns
793///
794/// Returns formatted lines ready for doc comment output.
795fn 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        // Check if this is a list item (bullet, numbered, or dash)
803        let is_bullet = trimmed.starts_with("* ") && !in_list;
804        let is_dash = trimmed.starts_with("- ") && !in_list;
805        let is_numbered = !in_list && {
806            // Match patterns like "1) ", "2. ", "10) ", etc.
807            if let Some(first_space) = trimmed.find(' ') {
808                let prefix = &trimmed[..first_space];
809                // Check if it ends with ) or . and starts with a number
810                (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            // We're in a list item context
822            if line.trim().is_empty() {
823                // Empty line ends the list item
824                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                // New list item
833                output.push(line.to_string());
834            } else {
835                // Continuation line - needs to be indented
836                let content = line.trim();
837                if !content.is_empty() {
838                    // For numbered lists like "1) text", indent to align with text
839                    // For bullet/dash lists, use 2 spaces
840                    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                                // It's a numbered list - use 3 spaces for safety
847                                "   ".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            // Not in a list item - regular line
862            output.push(line.to_string());
863        }
864    }
865    
866    output
867}
868
869/// Formats cardinality information into human-readable text.
870///
871/// # Arguments
872///
873/// * `min` - Minimum cardinality (0 or 1)
874/// * `max` - Maximum cardinality ("1", "*", or a specific number)
875///
876/// # Returns
877///
878/// Returns a formatted string describing the cardinality.
879fn 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
892/// Formats constraint information for documentation.
893///
894/// # Arguments
895///
896/// * `constraints` - Vector of ElementDefinitionConstraint
897///
898/// # Returns
899///
900/// Returns formatted constraint documentation.
901fn 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        // Handle multi-line constraint descriptions
913        let human_lines: Vec<&str> = escaped_human.split('\n').collect();
914        
915        if human_lines.len() == 1 {
916            // Single line - output as before
917            output.push_str(&format!("/// - **{}**: {} ({})\n", 
918                constraint.key,
919                escaped_human,
920                constraint.severity
921            ));
922        } else {
923            // Multi-line - format the first line with key and severity
924            output.push_str(&format!("/// - **{}**: {} ({})\n", 
925                constraint.key,
926                human_lines[0],
927                constraint.severity
928            ));
929            
930            // Add subsequent lines with proper indentation
931            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
947/// Formats example values for documentation.
948///
949/// # Arguments
950///
951/// * `examples` - Vector of ElementDefinitionExample
952///
953/// # Returns
954///
955/// Returns formatted example documentation.
956fn 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
974/// Formats binding information for documentation.
975///
976/// # Arguments
977///
978/// * `binding` - Optional ElementDefinitionBinding
979///
980/// # Returns
981///
982/// Returns formatted binding documentation.
983fn 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                // Single line - output as before
996                output.push_str(&format!("/// - **Description**: {}\n", escaped_desc));
997            } else {
998                // Multi-line - format the first line with "Description:"
999                output.push_str(&format!("/// - **Description**: {}\n", desc_lines[0]));
1000                
1001                // Add subsequent lines with proper indentation
1002                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
1021/// Generates documentation comments for a FHIR struct/type from its StructureDefinition.
1022///
1023/// This function extracts type-level documentation from a StructureDefinition.
1024///
1025/// # Arguments
1026///
1027/// * `sd` - The StructureDefinition to document
1028///
1029/// # Returns
1030///
1031/// Returns a string containing formatted Rust doc comments for the type.
1032fn generate_struct_documentation(sd: &StructureDefinition) -> String {
1033    let mut output = String::new();
1034    
1035    // Type name
1036    output.push_str(&format!("/// FHIR {} type\n", capitalize_first_letter(&sd.name)));
1037    
1038    // Description
1039    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            // Split long descriptions into multiple lines
1046            for line in formatted_lines {
1047                if line.is_empty() {
1048                    output.push_str("/// \n");
1049                } else if line.len() <= 77 {
1050                    // Line fits, output as is
1051                    output.push_str("/// ");
1052                    output.push_str(&line);
1053                    output.push('\n');
1054                } else {
1055                    // Need to wrap - use word boundaries
1056                    let words = line.split_whitespace().collect::<Vec<_>>();
1057                    let mut current_line = String::new();
1058                    
1059                    // Check if this line needs indentation
1060                    // Either it's already indented (continuation) or it's a list item
1061                    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                    // Determine if this is a numbered list that needs more indentation
1071                    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                        "   "  // Already has 3 spaces
1079                    } else if line.starts_with("  ") { 
1080                        "  "   // Already has 2 spaces
1081                    } else if is_numbered_list {
1082                        // For numbered lists, use 3 spaces for continuation lines
1083                        "   "
1084                    } else if is_list_item {
1085                        // For bullet/dash lists, use 2 spaces
1086                        "  "
1087                    } else { 
1088                        "" 
1089                    };
1090                    
1091                    // For list items, we don't want to indent the first line
1092                    let first_line_indent = if is_list_item { "" } else { indent };
1093                    
1094                    for word in words.iter() {
1095                        if current_line.is_empty() {
1096                            // First word - include indent if needed (but not for bullet points)
1097                            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 the current line
1107                            output.push_str("/// ");
1108                            output.push_str(&current_line);
1109                            output.push('\n');
1110                            // Start new line with this word, always use indent for continuations
1111                            current_line = if !indent.is_empty() {
1112                                format!("{}{}", indent, word)
1113                            } else {
1114                                word.to_string()
1115                            };
1116                        }
1117                    }
1118                    
1119                    // Output any remaining content
1120                    if !current_line.is_empty() {
1121                        output.push_str("/// ");
1122                        output.push_str(&current_line);
1123                        output.push('\n');
1124                    }
1125                }
1126            }
1127        }
1128    }
1129    
1130    // Purpose
1131    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    // Kind and base
1149    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    // Status and version
1161    output.push_str("/// \n");
1162    output.push_str(&format!("/// ## Status: {}\n", sd.status));
1163    
1164    // FHIR version
1165    if let Some(version) = &sd.fhir_version {
1166        output.push_str(&format!("/// FHIR Version: {}\n", version));
1167    }
1168    
1169    // URL reference
1170    output.push_str("/// \n");
1171    output.push_str(&format!("/// See: [{}]({})\n", sd.name, sd.url));
1172    
1173    output
1174}
1175
1176/// Generates comprehensive documentation comments for a FHIR element.
1177///
1178/// This function extracts all available documentation from an ElementDefinition
1179/// and formats it into structured Rust doc comments.
1180///
1181/// # Arguments
1182///
1183/// * `element` - The ElementDefinition to document
1184///
1185/// # Returns
1186///
1187/// Returns a string containing formatted Rust doc comments.
1188/// IMPORTANT: Every line in the returned string MUST start with "///"
1189fn generate_element_documentation(element: &ElementDefinition) -> String {
1190    let mut output = String::new();
1191    
1192    
1193    // Short description (primary doc comment)
1194    if let Some(short) = &element.short {
1195        output.push_str(&format!("/// {}\n", escape_doc_comment(short)));
1196    }
1197    
1198    // Full definition
1199    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            // Process each formatted line
1207            for line in formatted_lines {
1208                if line.is_empty() {
1209                    output.push_str("/// \n");
1210                } else if line.len() <= 77 {
1211                    // Line fits, output as is
1212                    output.push_str("/// ");
1213                    output.push_str(&line);
1214                    output.push('\n');
1215                    
1216                } else {
1217                    // Need to wrap - use word boundaries
1218                    let words = line.split_whitespace().collect::<Vec<_>>();
1219                    let mut current_line = String::new();
1220                    
1221                    // Check if this line needs indentation
1222                    // Either it's already indented (continuation) or it's a list item
1223                    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                    // Determine if this is a numbered list that needs more indentation
1233                    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                        "   "  // Already has 3 spaces
1241                    } else if line.starts_with("  ") { 
1242                        "  "   // Already has 2 spaces
1243                    } else if is_numbered_list {
1244                        // For numbered lists, use 3 spaces for continuation lines
1245                        "   "
1246                    } else if is_list_item {
1247                        // For bullet/dash lists, use 2 spaces
1248                        "  "
1249                    } else { 
1250                        "" 
1251                    };
1252                    
1253                    // For list items, we don't want to indent the first line
1254                    let first_line_indent = if is_list_item { "" } else { indent };
1255                    
1256                    for word in words.iter() {
1257                        if current_line.is_empty() {
1258                            // First word - include indent if needed (but not for bullet points)
1259                            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 the current line
1269                            output.push_str("/// ");
1270                            output.push_str(&current_line);
1271                            output.push('\n');
1272                            // Start new line with this word, always use indent for continuations
1273                            current_line = if !indent.is_empty() {
1274                                format!("{}{}", indent, word)
1275                            } else {
1276                                word.to_string()
1277                            };
1278                        }
1279                    }
1280                    
1281                    // Output any remaining content
1282                    if !current_line.is_empty() {
1283                        output.push_str("/// ");
1284                        output.push_str(&current_line);
1285                        output.push('\n');
1286                    }
1287                }
1288            }
1289        }
1290    }
1291    
1292    // Requirements
1293    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                    // Line fits, output as is
1305                    output.push_str("/// ");
1306                    output.push_str(&line);
1307                    output.push('\n');
1308                } else {
1309                    // Need to wrap - use word boundaries
1310                    let words = line.split_whitespace().collect::<Vec<_>>();
1311                    let mut current_line = String::new();
1312                    
1313                    // Check if this line needs indentation
1314                    // Either it's already indented (continuation) or it's a list item
1315                    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                    // Determine if this is a numbered list that needs more indentation
1325                    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                        "   "  // Already has 3 spaces
1333                    } else if line.starts_with("  ") { 
1334                        "  "   // Already has 2 spaces
1335                    } else if is_numbered_list {
1336                        // For numbered lists, use 3 spaces for continuation lines
1337                        "   "
1338                    } else if is_list_item {
1339                        // For bullet/dash lists, use 2 spaces
1340                        "  "
1341                    } else { 
1342                        "" 
1343                    };
1344                    
1345                    // For list items, we don't want to indent the first line
1346                    let first_line_indent = if is_list_item { "" } else { indent };
1347                    
1348                    for word in words.iter() {
1349                        if current_line.is_empty() {
1350                            // First word - include indent if needed (but not for bullet points)
1351                            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 the current line
1361                            output.push_str("/// ");
1362                            output.push_str(&current_line);
1363                            output.push('\n');
1364                            // Start new line with this word, always use indent for continuations
1365                            current_line = if !indent.is_empty() {
1366                                format!("{}{}", indent, word)
1367                            } else {
1368                                word.to_string()
1369                            };
1370                        }
1371                    }
1372                    
1373                    // Output any remaining content
1374                    if !current_line.is_empty() {
1375                        output.push_str("/// ");
1376                        output.push_str(&current_line);
1377                        output.push('\n');
1378                    }
1379                }
1380            }
1381        }
1382    }
1383    
1384    // Implementation comments
1385    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                    // Line fits, output as is
1398                    output.push_str("/// ");
1399                    output.push_str(&line);
1400                    output.push('\n');
1401                } else {
1402                    // Need to wrap - use word boundaries
1403                    let words = line.split_whitespace().collect::<Vec<_>>();
1404                    let mut current_line = String::new();
1405                    
1406                    // Check if this line needs indentation
1407                    // Either it's already indented (continuation) or it's a list item
1408                    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                    // Determine if this is a numbered list that needs more indentation
1418                    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                        "   "  // Already has 3 spaces
1426                    } else if line.starts_with("  ") { 
1427                        "  "   // Already has 2 spaces
1428                    } else if is_numbered_list {
1429                        // For numbered lists, use 3 spaces for continuation lines
1430                        "   "
1431                    } else if is_list_item {
1432                        // For bullet/dash lists, use 2 spaces
1433                        "  "
1434                    } else { 
1435                        "" 
1436                    };
1437                    
1438                    // For list items, we don't want to indent the first line
1439                    let first_line_indent = if is_list_item { "" } else { indent };
1440                    
1441                    for word in words.iter() {
1442                        if current_line.is_empty() {
1443                            // First word - include indent if needed (but not for bullet points)
1444                            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 the current line
1454                            output.push_str("/// ");
1455                            output.push_str(&current_line);
1456                            output.push('\n');
1457                            // Start new line with this word, always use indent for continuations
1458                            current_line = if !indent.is_empty() {
1459                                format!("{}{}", indent, word)
1460                            } else {
1461                                word.to_string()
1462                            };
1463                        }
1464                    }
1465                    
1466                    // Output any remaining content
1467                    if !current_line.is_empty() {
1468                        output.push_str("/// ");
1469                        output.push_str(&current_line);
1470                        output.push('\n');
1471                    }
1472                }
1473            }
1474        }
1475    }
1476    
1477    // Cardinality
1478    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    // Special semantics
1483    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    // Constraints
1518    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    // Examples
1527    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    // Binding
1536    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    // Aliases
1543    if let Some(aliases) = &element.alias {
1544        if !aliases.is_empty() {
1545            output.push_str("/// \n");
1546            output.push_str("/// ## Aliases\n");
1547            
1548            // Handle aliases that might contain newlines
1549            let all_aliases = aliases.join(", ");
1550            let escaped_aliases = escape_doc_comment(&all_aliases);
1551            
1552            // Split on newlines and ensure each line has the /// prefix
1553            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    // Conditions
1564    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    // Validate that all non-empty lines have the /// prefix
1573    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                // This should never happen, but if it does, add the prefix
1582                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
1597/// Converts a FHIR StructureDefinition to Rust code.
1598///
1599/// This function is the main entry point for converting a single StructureDefinition
1600/// into its corresponding Rust representation, handling both primitive and complex types.
1601///
1602/// # Arguments
1603///
1604/// * `sd` - The StructureDefinition to convert
1605/// * `cycles` - Set of detected circular dependencies that need special handling
1606///
1607/// # Returns
1608///
1609/// Returns a string containing the generated Rust code for this structure.
1610///
1611/// # Type Handling
1612///
1613/// - **Primitive types**: Generates type aliases using `Element<T, Extension>`
1614/// - **Complex types**: Generates full struct definitions with all fields
1615/// - **Resources**: Generates structs that can be included in the Resource enum
1616fn structure_definition_to_rust(
1617    sd: &StructureDefinition,
1618    cycles: &std::collections::HashSet<(String, String)>,
1619) -> String {
1620    let mut output = String::new();
1621
1622    // Handle primitive types differently
1623    if is_primitive_type(sd) {
1624        return generate_primitive_type(sd);
1625    }
1626
1627    // Generate struct documentation for the main type
1628    let struct_doc = generate_struct_documentation(sd);
1629    
1630    // Process elements for complex types and resources
1631    if let Some(snapshot) = &sd.snapshot {
1632        if let Some(elements) = &snapshot.element {
1633            let mut processed_types = std::collections::HashSet::new();
1634            // Find the root element to get its documentation
1635            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
1655/// Generates Rust type aliases for FHIR primitive types.
1656///
1657/// FHIR primitive types are mapped to appropriate Rust types and wrapped in
1658/// the `Element<T, Extension>` container to handle FHIR's extension mechanism.
1659///
1660/// # Arguments
1661///
1662/// * `sd` - The StructureDefinition for the primitive type
1663///
1664/// # Returns
1665///
1666/// Returns a string containing the type alias definition.
1667///
1668/// # Type Mappings
1669///
1670/// - `boolean` → `Element<bool, Extension>`
1671/// - `integer` → `Element<i32, Extension>`
1672/// - `decimal` → `DecimalElement<Extension>` (special handling for precision)
1673/// - `string`/`code`/`uri` → `Element<String, Extension>`
1674/// - Date/time types → `Element<PrecisionDate/DateTime/Time, Extension>` (precision-aware types)
1675///
1676/// # Note
1677///
1678/// This function must be kept in sync with `extract_inner_element_type` in
1679/// `fhir_macro/src/lib.rs` to ensure consistent type handling.
1680fn generate_primitive_type(sd: &StructureDefinition) -> String {
1681    let type_name = &sd.name;
1682    let mut output = String::new();
1683
1684    // Determine the value type based on the primitive type
1685    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    // Add type-specific documentation
1709    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    // Add description if available
1776    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    // Add reference to the spec
1784    output.push_str(&format!("/// \n/// See: [{}]({})\n", sd.name, sd.url));
1785    
1786    // Generate a type alias using Element<T, Extension> or DecimalElement<Extension> for decimal type
1787    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        // REMOVED From<T> generation from here to avoid conflicts
1796    }
1797
1798    output
1799}
1800
1801/// Detects circular dependencies between FHIR types.
1802///
1803/// This function analyzes ElementDefinitions to find circular references between
1804/// types where both directions have a cardinality of 1 (max="1"). Such cycles
1805/// would cause infinite-sized structs in Rust, so they need to be broken with
1806/// `Box<T>` pointers.
1807///
1808/// # Arguments
1809///
1810/// * `elements` - All ElementDefinitions to analyze for cycles
1811///
1812/// # Returns
1813///
1814/// Returns a set of tuples representing detected cycles. Each tuple contains
1815/// the two type names that form a cycle.
1816///
1817/// # Cycle Detection Logic
1818///
1819/// 1. Builds a dependency graph of type relationships with max="1"
1820/// 2. Finds bidirectional dependencies (A → B and B → A)
1821/// 3. Adds special cases like Bundle → Resource for known problematic cycles
1822///
1823/// # Example
1824///
1825/// If `Identifier` has a field of type `Reference` and `Reference` has a field
1826/// of type `Identifier`, both with max="1", this creates a cycle that must be
1827/// broken by boxing one of the references.
1828fn 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    // Build direct dependencies where max=1
1836    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    // Find cycles between exactly two structs
1856    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                    // We found a cycle between exactly two structs
1861                    cycles.insert((from_type.clone(), to_type.clone()));
1862                }
1863            }
1864        }
1865    }
1866
1867    // Add cycle from Bundle to Resource since Bundle.issues contains Resources (an specially generated enum) beginning in R5
1868    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
1878/// Processes ElementDefinitions to generate Rust struct and enum definitions.
1879///
1880/// This function groups related ElementDefinitions by their parent path and generates
1881/// the corresponding Rust types, including handling of choice types (polymorphic elements).
1882///
1883/// # Arguments
1884///
1885/// * `elements` - Slice of ElementDefinitions to process
1886/// * `output` - Mutable string to append generated code to
1887/// * `processed_types` - Set tracking which types have already been generated
1888/// * `cycles` - Set of detected circular dependencies requiring Box<T> handling
1889/// * `root_type_name` - The name of the root type (e.g., "Patient")
1890/// * `root_doc` - Optional documentation for the root type
1891///
1892/// # Process Overview
1893///
1894/// 1. **Grouping**: Groups elements by their parent path (e.g., "Patient.name")
1895/// 2. **Choice Types**: Generates enums for choice elements ending in "\[x\]"
1896/// 3. **Structs**: Generates struct definitions with all fields
1897/// 4. **Deduplication**: Ensures each type is only generated once
1898///
1899/// # Generated Code Features
1900///
1901/// - Derives for Debug, Clone, PartialEq, Eq, FhirSerde, FhirPath, Default
1902/// - Choice type enums with proper serde renaming
1903/// - Cycle-breaking with Box<T> where needed
1904/// - Optional wrapping for elements with min=0
1905fn 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    // Group elements by their parent path
1914    let mut element_groups: std::collections::HashMap<String, Vec<&ElementDefinition>> =
1915        std::collections::HashMap::new();
1916
1917    // First pass - collect all type names that will be generated
1918    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    // Process each group
1927    for (path, group) in element_groups {
1928        let type_name = generate_type_name(&path);
1929
1930        // Skip if we've already processed this type
1931        if processed_types.contains(&type_name) {
1932            continue;
1933        }
1934
1935        processed_types.insert(type_name.clone());
1936
1937        // Process choice types first
1938        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            // Skip if we've already processed this enum
1954            if processed_types.contains(&enum_name) {
1955                continue;
1956            }
1957            processed_types.insert(enum_name.clone());
1958
1959            // Add documentation comment for the enum
1960            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            // Generate enum derives - Add Clone, PartialEq, Eq to all enums
1967            let enum_derives = ["Debug", "Clone", "PartialEq", "Eq", "FhirSerde", "FhirPath"];
1968            output.push_str(&format!("#[derive({})]\n", enum_derives.join(", ")));
1969
1970            // Add choice element attribute to mark this as a choice type
1971            output.push_str(&format!(
1972                "#[fhir_choice_element(base_name = \"{}\")]\n",
1973                base_name
1974            ));
1975
1976            // Add other serde attributes and enum definition
1977            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                    // Add documentation for each variant
1985                    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        // Collect all choice element fields for this struct
2000        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        // Add struct documentation
2008        if path == *root_type_name {
2009            // This is the root type, use the provided documentation
2010            if let Some(doc) = root_doc {
2011                output.push_str(doc);
2012            }
2013        } else {
2014            // For nested types, try to find the documentation from the element
2015            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                // Generate a basic doc comment
2022                output.push_str(&format!("/// {} sub-type\n", capitalize_first_letter(&type_name)));
2023            }
2024        }
2025
2026        // Generate struct derives - Add Clone, PartialEq, Eq to all structs
2027        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        // Add fhir_resource attribute if there are choice elements
2039        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        // Add other serde attributes and struct definition
2048        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                    // For choice types, we've already created an enum, so we just need to add the field
2059                    // that uses that enum type. We don't need to expand each choice type into separate fields.
2060                    generate_element_definition(element, &type_name, output, cycles, elements);
2061                }
2062            }
2063        }
2064        output.push_str("}\n\n");
2065    }
2066}
2067
2068/// Generates a Rust field definition from a FHIR ElementDefinition.
2069///
2070/// This function converts a single FHIR element into a Rust struct field,
2071/// handling type mapping, cardinality, choice types, and circular references.
2072///
2073/// # Arguments
2074///
2075/// * `element` - The ElementDefinition to convert
2076/// * `type_name` - Name of the parent type containing this element
2077/// * `output` - Mutable string to append the field definition to
2078/// * `cycles` - Set of circular dependencies requiring Box<T> handling
2079/// * `elements` - All elements (used for resolving content references)
2080///
2081/// # Field Generation Features
2082///
2083/// - **Type Mapping**: Maps FHIR types to appropriate Rust types
2084/// - **Cardinality**: Wraps in `Option<T>` for min=0, `Vec<T>` for max="*"
2085/// - **Choice Types**: Uses generated enum types for polymorphic elements
2086/// - **Cycle Breaking**: Adds `Box<T>` for circular references
2087/// - **Serde Attributes**: Adds rename and flatten attributes as needed
2088/// - **Content References**: Resolves `#id` references to other elements
2089fn 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        // Handle field renaming, ensuring we don't add duplicate rename attributes
2101        if field_name != rust_field_name {
2102            // For choice fields, use the name without [x]
2103            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            // https://build.fhir.org/fhirpath.html#types
2140            "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            // Fix for R6 TestPlan: replace Base with BackboneElement
2151            // See https://github.com/HeliosSoftware/hfs/issues/11
2152            "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            // For choice fields, we use flatten instead of rename
2175            serde_attrs.clear(); // Clear any previous attributes
2176            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        // Add Box<> to break cycles (only to the "to" type in the cycle)
2187        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                        // Add Box<> around the type, preserving Option if present
2193                        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        // Generate documentation for this field
2205        let doc_comment = generate_element_documentation(element);
2206        if !doc_comment.is_empty() {
2207            // Debug: Check for any issues
2208            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            // Indent all doc comments with 4 spaces
2221            for line in doc_comment.lines() {
2222                // Ensure every line is a proper doc comment
2223                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                    // This line doesn't have a doc comment prefix - this is a bug!
2229                    eprintln!("WARNING: Doc comment line without /// prefix: {}", line);
2230                    output.push_str(&format!("    /// {}\n", line));
2231                }
2232            }
2233        }
2234
2235        // Output consolidated serde attributes if any exist
2236        if !serde_attrs.is_empty() {
2237            output.push_str(&format!("    #[fhir_serde({})]\n", serde_attrs.join(", ")));
2238        }
2239
2240        // For choice fields, strip the [x] from the field name
2241        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        // Check if the line would be too long (rustfmt's default max line width is 100)
2248        // Account for "    pub " (8 chars) + ": " (2 chars) + "," (1 char) = 11 extra chars
2249        let line_length = 8 + clean_field_name.len() + 2 + type_str.len() + 1;
2250
2251        if line_length > 100 {
2252            // For Option<Vec<...>>, rustfmt prefers a specific format
2253            if type_str.starts_with("Option<Vec<") && type_str.ends_with(">>") {
2254                // Extract the inner type
2255                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                // For other Option<...> types that are too long
2262                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                // Break other long type declarations across multiple lines
2269                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
2280/// Extracts the element ID from a contentReference value.
2281///
2282/// This function handles both local contentReferences (starting with #) and
2283/// URL-based contentReferences that include a fragment after #.
2284///
2285/// # Arguments
2286///
2287/// * `content_ref` - The contentReference value from a FHIR ElementDefinition
2288///
2289/// # Returns
2290///
2291/// Returns the element ID portion of the contentReference.
2292///
2293/// # Examples
2294///
2295/// - "#Patient.name" → "Patient.name"
2296/// - "https://sql-on-fhir.org/ig/StructureDefinition/ViewDefinition#ViewDefinition.select" → "ViewDefinition.select"
2297/// - "invalid-ref" → ""
2298fn 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
2307/// Generates a Rust type name from a FHIR element path.
2308///
2309/// This function converts dotted FHIR paths into appropriate Rust type names
2310/// using PascalCase conventions.
2311///
2312/// # Arguments
2313///
2314/// * `path` - The FHIR element path (e.g., "Patient.name.given")
2315///
2316/// # Returns
2317///
2318/// Returns a PascalCase type name suitable for Rust.
2319///
2320/// # Examples
2321///
2322/// - "Patient" → "Patient"
2323/// - "Patient.name" → "PatientName"
2324/// - "Observation.value.quantity" → "ObservationValueQuantity"
2325///
2326/// # Note
2327///
2328/// The first path segment becomes the base name, and subsequent segments
2329/// are capitalized and concatenated to create a compound type name.
2330fn 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        // Create a temporary directory for test output
2360        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        // Test processing R4 version
2364        assert!(process_fhir_version(Some(FhirVersion::R4), &temp_dir).is_ok());
2365
2366        // Verify files were created
2367        assert!(temp_dir.join("r4.rs").exists());
2368
2369        // Clean up
2370        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        // Should detect the Identifier <-> Reference cycle with both sides have max="1"
2437        // cardinality
2438        assert!(
2439            cycles.contains(&("Identifier".to_string(), "Reference".to_string()))
2440                || cycles.contains(&("Reference".to_string(), "Identifier".to_string()))
2441        );
2442
2443        // Should not detect Patient -> Resource as a cycle (one-way dependency)
2444        assert!(!cycles.contains(&("Patient".to_string(), "Resource".to_string())));
2445        assert!(!cycles.contains(&("Resource".to_string(), "Patient".to_string())));
2446
2447        // Should also not detect self cycles - these are ok
2448        assert!(!cycles.contains(&("Extension".to_string(), "Extension".to_string())));
2449
2450        // This is ok too because it is a one to many relationship.
2451        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                    // Verify that we have something
2467                    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                    // Verify we have the expected type definitions
2476                    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}