Skip to main content

rh_codegen/generators/
cardinality_generator.rs

1//! Cardinality constant generation
2//!
3//! This module generates constant declarations for FHIR element cardinalities that can be
4//! embedded in generated resource and datatype structs.
5
6use crate::fhir_types::StructureDefinition;
7
8/// Generator for cardinality constants
9pub struct CardinalityGenerator;
10
11impl CardinalityGenerator {
12    /// Generate a CARDINALITIES constant for a StructureDefinition
13    ///
14    /// Returns Rust code as a string containing the static declaration.
15    ///
16    /// Uses `once_cell::sync::Lazy` for runtime initialization.
17    ///
18    /// Example output:
19    /// ```rust,ignore
20    /// pub static CARDINALITIES: once_cell::sync::Lazy<Vec<rh_foundation::ElementCardinality>> =
21    ///     once_cell::sync::Lazy::new(|| vec![
22    ///         rh_foundation::ElementCardinality::new("Patient.identifier", 0, None),
23    ///         rh_foundation::ElementCardinality::new("Patient.active", 0, Some(1)),
24    ///     ]);
25    /// ```
26    pub fn generate_cardinalities_constant(structure_def: &StructureDefinition) -> String {
27        let cardinalities = Self::extract_cardinalities(structure_def);
28
29        let mut code = String::new();
30        code.push_str("/// FHIR cardinality constraints for this resource/datatype\n");
31        code.push_str("///\n");
32        code.push_str(
33            "/// These define the minimum and maximum occurrences allowed for each element.\n",
34        );
35        if cardinalities.is_empty() {
36            code.push_str("pub static CARDINALITIES: once_cell::sync::Lazy<Vec<rh_foundation::ElementCardinality>> = once_cell::sync::Lazy::new(Vec::new);");
37            code.push('\n');
38            return code;
39        }
40
41        code.push_str("pub static CARDINALITIES: once_cell::sync::Lazy<Vec<rh_foundation::ElementCardinality>> = once_cell::sync::Lazy::new(|| vec![\n");
42
43        for card in &cardinalities {
44            let path = escape_rust_string(&card.path);
45            let max_str = match card.max {
46                None => "None".to_string(),
47                Some(n) => format!("Some({n})"),
48            };
49
50            code.push_str(&format!(
51                "    rh_foundation::ElementCardinality::new(\"{path}\", {}, {max_str}),\n",
52                card.min
53            ));
54        }
55
56        code.push_str("]);\n");
57        code
58    }
59
60    /// Extract cardinality constraints from a StructureDefinition
61    ///
62    /// Returns a vector of ElementCardinality structs for all elements in the resource.
63    fn extract_cardinalities(
64        structure_def: &StructureDefinition,
65    ) -> Vec<rh_foundation::validation::ElementCardinality> {
66        let mut cardinalities = Vec::new();
67
68        // Get the base type name (e.g., "Patient" from "Patient" or "http://hl7.org/fhir/StructureDefinition/Patient")
69        let base_type = structure_def.name.as_str();
70
71        // Process snapshot elements if available
72        if let Some(snapshot) = &structure_def.snapshot {
73            for element in &snapshot.element {
74                // Skip the root element (e.g., just "Patient")
75                if element.path == base_type {
76                    continue;
77                }
78
79                // Only process direct child elements (not nested BackboneElement sub-elements for now)
80                // We can enhance this later to handle nested elements
81                if element.path.matches('.').count() > 1 {
82                    // This is a nested element like "Patient.contact.name"
83                    // For Phase 9, we'll handle these but need to be careful about the path
84                    // For now, include them all
85                }
86
87                let min = element.min.unwrap_or(0) as usize;
88                let max = if let Some(max_str) = &element.max {
89                    if max_str == "*" {
90                        None // Unbounded
91                    } else {
92                        max_str.parse::<usize>().ok()
93                    }
94                } else {
95                    Some(1) // Default to 1 if not specified
96                };
97
98                cardinalities.push(rh_foundation::validation::ElementCardinality::new(
99                    element.path.clone(),
100                    min,
101                    max,
102                ));
103            }
104        }
105
106        cardinalities
107    }
108}
109
110/// Escape a string for use in a Rust string literal
111fn escape_rust_string(s: &str) -> String {
112    s.replace('\\', "\\\\")
113        .replace('"', "\\\"")
114        .replace('\n', "\\n")
115        .replace('\r', "\\r")
116        .replace('\t', "\\t")
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_escape_rust_string() {
125        assert_eq!(escape_rust_string("hello"), "hello");
126        assert_eq!(escape_rust_string("hello \"world\""), "hello \\\"world\\\"");
127        assert_eq!(escape_rust_string("path\\to\\file"), "path\\\\to\\\\file");
128    }
129}