1use crate::fhir_types::StructureDefinition;
7use crate::invariants;
8use quote::quote;
9
10pub struct InvariantGenerator;
12
13impl InvariantGenerator {
14 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 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 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 #[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 pub const INVARIANTS: &[rh_foundation::Invariant] = &[
129 #(#invariant_items),*
130 ];
131 }
132 }
133}
134
135fn 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}