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