Skip to main content

rh_codegen/generators/
invariant_generator.rs

1//! Invariant constant generation
2//!
3//! This module generates constant declarations for FHIR invariants that can be
4//! embedded in generated resource and datatype structs.
5
6use crate::fhir_types::StructureDefinition;
7use crate::invariants;
8use quote::quote;
9
10/// Generator for invariant constants
11pub struct InvariantGenerator;
12
13impl InvariantGenerator {
14    /// Generate a INVARIANTS constant for a StructureDefinition
15    ///
16    /// Returns Rust code as a string containing the lazy static declaration,
17    /// or an empty string if there are no invariants.
18    ///
19    /// Uses `once_cell::sync::Lazy` for runtime initialization since `Invariant::new()`
20    /// is not a const fn (it uses `.into()` for String conversions).
21    ///
22    /// Example output:
23    /// ```rust,ignore
24    /// pub static INVARIANTS: once_cell::sync::Lazy<Vec<rh_foundation::Invariant>> =
25    ///     once_cell::sync::Lazy::new(|| vec![
26    ///         rh_foundation::Invariant::new(
27    ///             "pat-1",
28    ///             rh_foundation::Severity::Error,
29    ///             "SHALL at least contain a contact's details or a reference to an organization",
30    ///             "name.exists() or telecom.exists() or address.exists() or organization.exists()"
31    ///         ).with_xpath(""),
32    ///     ]);
33    /// ```
34    pub fn generate_invariants_constant(structure_def: &StructureDefinition) -> String {
35        let invariants = invariants::extract_invariants(structure_def);
36
37        if invariants.is_empty() {
38            return String::new();
39        }
40
41        let mut code = String::new();
42        code.push_str("/// FHIR invariants for this resource/datatype\n");
43        code.push_str("///\n");
44        code.push_str(
45            "/// These constraints are defined in the FHIR specification and must be validated\n",
46        );
47        code.push_str("/// when creating or modifying instances of this type.\n");
48        code.push_str("pub static INVARIANTS: once_cell::sync::Lazy<Vec<rh_foundation::Invariant>> = once_cell::sync::Lazy::new(|| vec![\n");
49
50        for invariant in &invariants {
51            // Escape strings for Rust string literals
52            let key = escape_rust_string(&invariant.key);
53            let human = escape_rust_string(&invariant.human);
54            let expression = escape_rust_string(&invariant.expression);
55
56            let severity = match invariant.severity {
57                rh_foundation::Severity::Error => "rh_foundation::Severity::Error",
58                rh_foundation::Severity::Warning => "rh_foundation::Severity::Warning",
59                rh_foundation::Severity::Information => "rh_foundation::Severity::Information",
60            };
61
62            // Use the builder pattern: Invariant::new().with_xpath() if xpath exists
63            code.push_str(&format!(
64                "    rh_foundation::Invariant::new(\"{key}\", {severity}, \"{human}\", \"{expression}\")"
65            ));
66
67            if let Some(xpath) = &invariant.xpath {
68                let escaped_xpath = escape_rust_string(xpath);
69                code.push_str(&format!(".with_xpath(\"{escaped_xpath}\")"));
70            }
71
72            code.push_str(",\n");
73        }
74
75        code.push_str("]);\n");
76        code
77    }
78
79    /// Generate invariants constant using quote! macro (alternative implementation)
80    ///
81    /// This generates the same output but uses proc_macro2::TokenStream,
82    /// which can be useful for integration with other token-based code generation.
83    #[allow(dead_code)]
84    pub fn generate_invariants_tokens(
85        structure_def: &StructureDefinition,
86    ) -> proc_macro2::TokenStream {
87        let invariants = invariants::extract_invariants(structure_def);
88
89        if invariants.is_empty() {
90            return quote! {};
91        }
92
93        let invariant_items: Vec<_> = invariants
94            .iter()
95            .map(|inv| {
96                let key = &inv.key;
97                let human = &inv.human;
98                let expression = &inv.expression;
99
100                let severity = match inv.severity {
101                    rh_foundation::Severity::Error => {
102                        quote! { rh_foundation::Severity::Error }
103                    }
104                    rh_foundation::Severity::Warning => {
105                        quote! { rh_foundation::Severity::Warning }
106                    }
107                    rh_foundation::Severity::Information => {
108                        quote! { rh_foundation::Severity::Information }
109                    }
110                };
111
112                quote! {
113                    rh_foundation::Invariant {
114                        key: #key,
115                        severity: #severity,
116                        human: #human,
117                        expression: #expression,
118                    }
119                }
120            })
121            .collect();
122
123        quote! {
124            /// FHIR invariants for this resource/datatype
125            ///
126            /// These constraints are defined in the FHIR specification and must be validated
127            /// when creating or modifying instances of this type.
128            pub const INVARIANTS: &[rh_foundation::Invariant] = &[
129                #(#invariant_items),*
130            ];
131        }
132    }
133}
134
135/// Escape a string for use in a Rust string literal
136fn escape_rust_string(s: &str) -> String {
137    s.chars()
138        .flat_map(|c| match c {
139            '"' => vec!['\\', '"'],
140            '\\' => vec!['\\', '\\'],
141            '\n' => vec!['\\', 'n'],
142            '\r' => vec!['\\', 'r'],
143            '\t' => vec!['\\', 't'],
144            c => vec![c],
145        })
146        .collect()
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::fhir_types::{ElementConstraint, ElementDefinition, StructureDefinitionSnapshot};
153
154    fn create_test_structure_def(constraints: Vec<ElementConstraint>) -> StructureDefinition {
155        let element = ElementDefinition {
156            id: Some("Patient".to_string()),
157            path: "Patient".to_string(),
158            short: None,
159            definition: None,
160            min: None,
161            max: None,
162            element_type: None,
163            fixed: None,
164            pattern: None,
165            binding: None,
166            constraint: Some(constraints),
167        };
168
169        StructureDefinition {
170            resource_type: "StructureDefinition".to_string(),
171            id: "Patient".to_string(),
172            url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
173            version: Some("4.0.1".to_string()),
174            name: "Patient".to_string(),
175            title: Some("Patient".to_string()),
176            status: "active".to_string(),
177            description: None,
178            purpose: None,
179            kind: "resource".to_string(),
180            is_abstract: false,
181            base_type: "Patient".to_string(),
182            base_definition: None,
183            differential: None,
184            snapshot: Some(StructureDefinitionSnapshot {
185                element: vec![element],
186            }),
187        }
188    }
189
190    #[test]
191    fn test_generate_empty_invariants() {
192        let structure_def = create_test_structure_def(vec![]);
193        let code = InvariantGenerator::generate_invariants_constant(&structure_def);
194        assert_eq!(code, "");
195    }
196
197    #[test]
198    fn test_generate_single_invariant() {
199        let constraint = ElementConstraint {
200            key: "pat-1".to_string(),
201            severity: "error".to_string(),
202            human: "Name is required".to_string(),
203            expression: Some("name.exists()".to_string()),
204            xpath: None,
205            source: None,
206        };
207
208        let structure_def = create_test_structure_def(vec![constraint]);
209        let code = InvariantGenerator::generate_invariants_constant(&structure_def);
210
211        assert!(code.contains("pub static INVARIANTS"));
212        assert!(code.contains("once_cell::sync::Lazy"));
213        assert!(code.contains("\"pat-1\""));
214        assert!(code.contains("rh_foundation::Severity::Error"));
215        assert!(code.contains("\"Name is required\""));
216        assert!(code.contains("\"name.exists()\""));
217    }
218
219    #[test]
220    fn test_generate_multiple_invariants() {
221        let constraints = vec![
222            ElementConstraint {
223                key: "pat-1".to_string(),
224                severity: "error".to_string(),
225                human: "Name required".to_string(),
226                expression: Some("name.exists()".to_string()),
227                xpath: None,
228                source: None,
229            },
230            ElementConstraint {
231                key: "pat-2".to_string(),
232                severity: "warning".to_string(),
233                human: "Telecom recommended".to_string(),
234                expression: Some("telecom.exists()".to_string()),
235                xpath: None,
236                source: None,
237            },
238        ];
239
240        let structure_def = create_test_structure_def(constraints);
241        let code = InvariantGenerator::generate_invariants_constant(&structure_def);
242
243        assert!(code.contains("pat-1"));
244        assert!(code.contains("pat-2"));
245        assert!(code.contains("rh_foundation::Severity::Error"));
246        assert!(code.contains("rh_foundation::Severity::Warning"));
247    }
248
249    #[test]
250    fn test_escape_strings_with_quotes() {
251        let constraint = ElementConstraint {
252            key: "test-1".to_string(),
253            severity: "error".to_string(),
254            human: "Must have \"value\"".to_string(),
255            expression: Some("value.exists()".to_string()),
256            xpath: None,
257            source: None,
258        };
259
260        let structure_def = create_test_structure_def(vec![constraint]);
261        let code = InvariantGenerator::generate_invariants_constant(&structure_def);
262
263        assert!(code.contains("Must have \\\"value\\\""));
264    }
265
266    #[test]
267    fn test_escape_strings_with_backslashes() {
268        let constraint = ElementConstraint {
269            key: "test-1".to_string(),
270            severity: "error".to_string(),
271            human: "Path: C:\\temp\\file".to_string(),
272            expression: Some("true".to_string()),
273            xpath: None,
274            source: None,
275        };
276
277        let structure_def = create_test_structure_def(vec![constraint]);
278        let code = InvariantGenerator::generate_invariants_constant(&structure_def);
279
280        assert!(code.contains("C:\\\\temp\\\\file"));
281    }
282
283    #[test]
284    fn test_tokens_generation() {
285        let constraint = ElementConstraint {
286            key: "pat-1".to_string(),
287            severity: "error".to_string(),
288            human: "Name required".to_string(),
289            expression: Some("name.exists()".to_string()),
290            xpath: None,
291            source: None,
292        };
293
294        let structure_def = create_test_structure_def(vec![constraint]);
295        let tokens = InvariantGenerator::generate_invariants_tokens(&structure_def);
296
297        let code = tokens.to_string();
298        assert!(code.contains("INVARIANTS"));
299        assert!(code.contains("pat-1"));
300    }
301
302    #[test]
303    fn test_severity_mapping() {
304        let constraints = vec![
305            ElementConstraint {
306                key: "err-1".to_string(),
307                severity: "error".to_string(),
308                human: "Error test".to_string(),
309                expression: Some("true".to_string()),
310                xpath: None,
311                source: None,
312            },
313            ElementConstraint {
314                key: "warn-1".to_string(),
315                severity: "warning".to_string(),
316                human: "Warning test".to_string(),
317                expression: Some("true".to_string()),
318                xpath: None,
319                source: None,
320            },
321            ElementConstraint {
322                key: "info-1".to_string(),
323                severity: "information".to_string(),
324                human: "Info test".to_string(),
325                expression: Some("true".to_string()),
326                xpath: None,
327                source: None,
328            },
329        ];
330
331        let structure_def = create_test_structure_def(constraints);
332        let code = InvariantGenerator::generate_invariants_constant(&structure_def);
333
334        assert!(code.contains("rh_foundation::Severity::Error"));
335        assert!(code.contains("rh_foundation::Severity::Warning"));
336        assert!(code.contains("rh_foundation::Severity::Information"));
337    }
338}