Skip to main content

rh_codegen/
invariants.rs

1//! Invariant extraction from FHIR StructureDefinitions
2//!
3//! This module extracts invariants (constraints) from FHIR StructureDefinitions
4//! and converts them to Rust Invariant structs for use in validation.
5
6use crate::fhir_types::{ElementConstraint, StructureDefinition};
7use rh_foundation::{Invariant, Severity};
8
9/// Extract all invariants from a StructureDefinition
10///
11/// This function processes both the snapshot and differential elements,
12/// extracting constraints and converting them to Invariant structs.
13/// Only FHIRPath expressions are included (xpath is ignored as legacy).
14pub fn extract_invariants(structure_def: &StructureDefinition) -> Vec<Invariant> {
15    let mut invariants = Vec::new();
16
17    if let Some(ref snapshot) = structure_def.snapshot {
18        for element in &snapshot.element {
19            if let Some(ref constraints) = element.constraint {
20                for constraint in constraints {
21                    if let Some(invariant) = convert_constraint(constraint) {
22                        if !invariants
23                            .iter()
24                            .any(|i: &Invariant| i.key == invariant.key)
25                        {
26                            invariants.push(invariant);
27                        }
28                    }
29                }
30            }
31        }
32    }
33
34    if let Some(ref differential) = structure_def.differential {
35        for element in &differential.element {
36            if let Some(ref constraints) = element.constraint {
37                for constraint in constraints {
38                    if let Some(invariant) = convert_constraint(constraint) {
39                        if !invariants
40                            .iter()
41                            .any(|i: &Invariant| i.key == invariant.key)
42                        {
43                            invariants.push(invariant);
44                        }
45                    }
46                }
47            }
48        }
49    }
50    invariants.sort_by(|a, b| a.key.cmp(&b.key));
51    invariants
52}
53
54/// Convert a FHIR ElementConstraint to an Invariant
55///
56/// Returns None if the constraint has no FHIRPath expression (xpath-only constraints
57/// are ignored as legacy).
58fn convert_constraint(constraint: &ElementConstraint) -> Option<Invariant> {
59    let expression = constraint.expression.as_ref()?;
60
61    let severity = match constraint.severity.as_str() {
62        "error" => Severity::Error,
63        "warning" => Severity::Warning,
64        _ => Severity::Information,
65    };
66
67    let mut invariant = Invariant::new(&constraint.key, severity, &constraint.human, expression);
68
69    if let Some(xpath) = &constraint.xpath {
70        invariant = invariant.with_xpath(xpath);
71    }
72
73    Some(invariant)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::fhir_types::{
80        ElementDefinition, StructureDefinitionDifferential, StructureDefinitionSnapshot,
81    };
82
83    fn create_test_constraint(
84        key: &str,
85        severity: &str,
86        human: &str,
87        expression: Option<&str>,
88    ) -> ElementConstraint {
89        ElementConstraint {
90            key: key.to_string(),
91            severity: severity.to_string(),
92            human: human.to_string(),
93            expression: expression.map(|s| s.to_string()),
94            xpath: None,
95            source: None,
96        }
97    }
98
99    fn create_test_element(constraints: Vec<ElementConstraint>) -> ElementDefinition {
100        ElementDefinition {
101            id: Some("Patient".to_string()),
102            path: "Patient".to_string(),
103            short: None,
104            definition: None,
105            min: None,
106            max: None,
107            element_type: None,
108            fixed: None,
109            pattern: None,
110            binding: None,
111            constraint: Some(constraints),
112        }
113    }
114
115    #[test]
116    fn test_convert_constraint_error() {
117        let constraint =
118            create_test_constraint("pat-1", "error", "Name is required", Some("name.exists()"));
119
120        let invariant = convert_constraint(&constraint).unwrap();
121        assert_eq!(invariant.key, "pat-1");
122        assert_eq!(invariant.severity, Severity::Error);
123        assert_eq!(invariant.human, "Name is required");
124        assert_eq!(invariant.expression, "name.exists()");
125    }
126
127    #[test]
128    fn test_convert_constraint_warning() {
129        let constraint = create_test_constraint(
130            "pat-2",
131            "warning",
132            "Telecom recommended",
133            Some("telecom.exists()"),
134        );
135
136        let invariant = convert_constraint(&constraint).unwrap();
137        assert_eq!(invariant.severity, Severity::Warning);
138    }
139
140    #[test]
141    fn test_convert_constraint_no_expression() {
142        let constraint = ElementConstraint {
143            key: "pat-1".to_string(),
144            severity: "error".to_string(),
145            human: "Name is required".to_string(),
146            expression: None,
147            xpath: Some("f:name".to_string()),
148            source: None,
149        };
150
151        let invariant = convert_constraint(&constraint);
152        assert!(invariant.is_none());
153    }
154
155    #[test]
156    fn test_extract_invariants_from_snapshot() {
157        let constraints = vec![
158            create_test_constraint("pat-1", "error", "Name required", Some("name.exists()")),
159            create_test_constraint(
160                "pat-2",
161                "warning",
162                "Telecom recommended",
163                Some("telecom.exists()"),
164            ),
165        ];
166
167        let element = create_test_element(constraints);
168        let structure_def = StructureDefinition {
169            resource_type: "StructureDefinition".to_string(),
170            id: "Patient".to_string(),
171            url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
172            version: Some("4.0.1".to_string()),
173            name: "Patient".to_string(),
174            title: Some("Patient".to_string()),
175            status: "active".to_string(),
176            description: None,
177            purpose: None,
178            kind: "resource".to_string(),
179            is_abstract: false,
180            base_type: "Patient".to_string(),
181            base_definition: None,
182            differential: None,
183            snapshot: Some(StructureDefinitionSnapshot {
184                element: vec![element],
185            }),
186        };
187
188        let invariants = extract_invariants(&structure_def);
189        assert_eq!(invariants.len(), 2);
190        assert_eq!(invariants[0].key, "pat-1");
191        assert_eq!(invariants[1].key, "pat-2");
192    }
193
194    #[test]
195    fn test_extract_invariants_deduplication() {
196        let constraints = vec![create_test_constraint(
197            "pat-1",
198            "error",
199            "Name required",
200            Some("name.exists()"),
201        )];
202
203        let element1 = create_test_element(constraints.clone());
204        let element2 = create_test_element(constraints);
205
206        let structure_def = StructureDefinition {
207            resource_type: "StructureDefinition".to_string(),
208            id: "Patient".to_string(),
209            url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
210            version: Some("4.0.1".to_string()),
211            name: "Patient".to_string(),
212            title: Some("Patient".to_string()),
213            status: "active".to_string(),
214            description: None,
215            purpose: None,
216            kind: "resource".to_string(),
217            is_abstract: false,
218            base_type: "Patient".to_string(),
219            base_definition: None,
220            differential: Some(StructureDefinitionDifferential {
221                element: vec![element1],
222            }),
223            snapshot: Some(StructureDefinitionSnapshot {
224                element: vec![element2],
225            }),
226        };
227
228        let invariants = extract_invariants(&structure_def);
229        assert_eq!(invariants.len(), 1);
230        assert_eq!(invariants[0].key, "pat-1");
231    }
232
233    #[test]
234    fn test_extract_invariants_sorted() {
235        let constraints = vec![
236            create_test_constraint("pat-3", "error", "Test 3", Some("true")),
237            create_test_constraint("pat-1", "error", "Test 1", Some("true")),
238            create_test_constraint("pat-2", "error", "Test 2", Some("true")),
239        ];
240
241        let element = create_test_element(constraints);
242        let structure_def = StructureDefinition {
243            resource_type: "StructureDefinition".to_string(),
244            id: "Test".to_string(),
245            url: "http://example.com/Test".to_string(),
246            version: None,
247            name: "Test".to_string(),
248            title: None,
249            status: "active".to_string(),
250            description: None,
251            purpose: None,
252            kind: "resource".to_string(),
253            is_abstract: false,
254            base_type: "Test".to_string(),
255            base_definition: None,
256            differential: None,
257            snapshot: Some(StructureDefinitionSnapshot {
258                element: vec![element],
259            }),
260        };
261
262        let invariants = extract_invariants(&structure_def);
263        assert_eq!(invariants.len(), 3);
264        assert_eq!(invariants[0].key, "pat-1");
265        assert_eq!(invariants[1].key, "pat-2");
266        assert_eq!(invariants[2].key, "pat-3");
267    }
268
269    #[test]
270    fn test_extract_invariants_empty() {
271        let structure_def = StructureDefinition {
272            resource_type: "StructureDefinition".to_string(),
273            id: "Test".to_string(),
274            url: "http://example.com/Test".to_string(),
275            version: None,
276            name: "Test".to_string(),
277            title: None,
278            status: "active".to_string(),
279            description: None,
280            purpose: None,
281            kind: "resource".to_string(),
282            is_abstract: false,
283            base_type: "Test".to_string(),
284            base_definition: None,
285            differential: None,
286            snapshot: None,
287        };
288
289        let invariants = extract_invariants(&structure_def);
290        assert_eq!(invariants.len(), 0);
291    }
292}