Skip to main content

rh_codegen/
bindings.rs

1//! Binding extraction from FHIR StructureDefinitions
2//!
3//! This module extracts element bindings (especially required bindings) from FHIR
4//! StructureDefinitions and prepares them for code generation.
5
6use crate::fhir_types::{ElementDefinition, StructureDefinition};
7use rh_foundation::validation::{BindingStrength, ElementBinding};
8use std::collections::HashMap;
9
10/// Extract required bindings from a StructureDefinition
11///
12/// This function extracts all element bindings with "required" strength from
13/// the StructureDefinition's snapshot. Only required bindings are extracted
14/// because they must be validated at runtime.
15///
16/// # Arguments
17///
18/// * `sd` - The StructureDefinition to extract bindings from
19///
20/// # Returns
21///
22/// A vector of ElementBinding structs, sorted by element path
23pub fn extract_required_bindings(sd: &StructureDefinition) -> Vec<ElementBinding> {
24    let mut bindings = Vec::new();
25    let mut seen = HashMap::new();
26
27    // Process snapshot (canonical representation)
28    if let Some(snapshot) = &sd.snapshot {
29        for element in &snapshot.element {
30            if let Some(binding) = extract_binding_from_element(element, &sd.base_type) {
31                // Only include required bindings
32                if binding.strength == BindingStrength::Required {
33                    // Deduplicate by path (snapshot can have duplicates)
34                    if !seen.contains_key(&binding.path) {
35                        seen.insert(binding.path.clone(), binding.clone());
36                        bindings.push(binding);
37                    }
38                }
39            }
40        }
41    }
42
43    // Sort by path for deterministic output
44    bindings.sort_by(|a, b| a.path.cmp(&b.path));
45    bindings
46}
47
48/// Extract binding from a single element definition
49fn extract_binding_from_element(
50    element: &ElementDefinition,
51    _resource_type: &str,
52) -> Option<ElementBinding> {
53    let binding = element.binding.as_ref()?;
54    let value_set_url = binding.value_set.as_ref()?;
55
56    // Parse binding strength
57    let strength = BindingStrength::from_fhir_str(&binding.strength)?;
58
59    // Get element path
60    let path = element.path.clone();
61
62    // Create binding with optional description
63    let mut elem_binding = ElementBinding::new(path, strength, value_set_url);
64    if let Some(desc) = &binding.description {
65        elem_binding = elem_binding.with_description(desc);
66    }
67
68    Some(elem_binding)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::fhir_types::{ElementBinding as FhirElementBinding, StructureDefinitionSnapshot};
75
76    fn make_test_element(path: &str, strength: &str, value_set: &str) -> ElementDefinition {
77        ElementDefinition {
78            id: Some(path.to_string()),
79            path: path.to_string(),
80            short: None,
81            definition: None,
82            min: None,
83            max: None,
84            element_type: None,
85            fixed: None,
86            pattern: None,
87            binding: Some(FhirElementBinding {
88                strength: strength.to_string(),
89                description: Some("Test binding".to_string()),
90                value_set: Some(value_set.to_string()),
91            }),
92            constraint: None,
93        }
94    }
95
96    fn make_test_sd(elements: Vec<ElementDefinition>) -> StructureDefinition {
97        StructureDefinition {
98            resource_type: "StructureDefinition".to_string(),
99            id: "test".to_string(),
100            url: "http://test.com/test".to_string(),
101            version: None,
102            name: "Test".to_string(),
103            title: None,
104            status: "active".to_string(),
105            description: None,
106            purpose: None,
107            kind: "resource".to_string(),
108            is_abstract: false,
109            base_type: "Patient".to_string(),
110            base_definition: None,
111            differential: None,
112            snapshot: Some(StructureDefinitionSnapshot { element: elements }),
113        }
114    }
115
116    #[test]
117    fn test_extract_required_binding() {
118        let element = make_test_element(
119            "Patient.gender",
120            "required",
121            "http://hl7.org/fhir/ValueSet/administrative-gender",
122        );
123        let sd = make_test_sd(vec![element]);
124
125        let bindings = extract_required_bindings(&sd);
126
127        assert_eq!(bindings.len(), 1);
128        assert_eq!(bindings[0].path, "Patient.gender");
129        assert_eq!(bindings[0].strength, BindingStrength::Required);
130        assert_eq!(
131            bindings[0].value_set_url,
132            "http://hl7.org/fhir/ValueSet/administrative-gender"
133        );
134        assert!(bindings[0].description.is_some());
135    }
136
137    #[test]
138    fn test_extract_multiple_required_bindings() {
139        let elements = vec![
140            make_test_element(
141                "Patient.gender",
142                "required",
143                "http://hl7.org/fhir/ValueSet/administrative-gender",
144            ),
145            make_test_element(
146                "Patient.contact.gender",
147                "required",
148                "http://hl7.org/fhir/ValueSet/administrative-gender",
149            ),
150        ];
151        let sd = make_test_sd(elements);
152
153        let bindings = extract_required_bindings(&sd);
154
155        assert_eq!(bindings.len(), 2);
156        // Should be sorted by path
157        assert_eq!(bindings[0].path, "Patient.contact.gender");
158        assert_eq!(bindings[1].path, "Patient.gender");
159    }
160
161    #[test]
162    fn test_skip_non_required_bindings() {
163        let elements = vec![
164            make_test_element(
165                "Patient.gender",
166                "required",
167                "http://hl7.org/fhir/ValueSet/administrative-gender",
168            ),
169            make_test_element(
170                "Patient.maritalStatus",
171                "extensible",
172                "http://hl7.org/fhir/ValueSet/marital-status",
173            ),
174            make_test_element(
175                "Patient.communication.language",
176                "preferred",
177                "http://hl7.org/fhir/ValueSet/languages",
178            ),
179        ];
180        let sd = make_test_sd(elements);
181
182        let bindings = extract_required_bindings(&sd);
183
184        // Only required bindings should be extracted
185        assert_eq!(bindings.len(), 1);
186        assert_eq!(bindings[0].path, "Patient.gender");
187    }
188
189    #[test]
190    fn test_deduplicate_bindings() {
191        // Same binding appears twice (can happen in snapshot)
192        let element = make_test_element(
193            "Patient.gender",
194            "required",
195            "http://hl7.org/fhir/ValueSet/administrative-gender",
196        );
197        let sd = make_test_sd(vec![element.clone(), element]);
198
199        let bindings = extract_required_bindings(&sd);
200
201        // Should only appear once
202        assert_eq!(bindings.len(), 1);
203    }
204
205    #[test]
206    fn test_no_bindings() {
207        let element = ElementDefinition {
208            id: Some("Patient.id".to_string()),
209            path: "Patient.id".to_string(),
210            short: None,
211            definition: None,
212            min: None,
213            max: None,
214            element_type: None,
215            fixed: None,
216            pattern: None,
217            binding: None,
218            constraint: None,
219        };
220        let sd = make_test_sd(vec![element]);
221
222        let bindings = extract_required_bindings(&sd);
223
224        assert_eq!(bindings.len(), 0);
225    }
226}