Skip to main content

rh_codegen/generators/
validation_trait_generator.rs

1//! Validation trait generation
2//!
3//! This module generates the ValidatableResource trait and implementations
4//! for FHIR resources and complex types that have invariants.
5
6use crate::fhir_types::StructureDefinition;
7use crate::invariants;
8
9/// Generator for validation traits and implementations
10pub struct ValidationTraitGenerator;
11
12impl ValidationTraitGenerator {
13    /// Generate the ValidatableResource trait definition
14    ///
15    /// This trait provides access to invariants for validation purposes.
16    ///
17    /// Example output:
18    /// ```rust,ignore
19    /// pub trait ValidatableResource {
20    ///     fn resource_type(&self) -> &'static str;
21    ///     fn invariants() -> &'static [rh_foundation::Invariant];
22    ///     fn bindings() -> &'static [rh_foundation::ElementBinding] {
23    ///         &[]
24    ///     }
25    ///     fn profile_url() -> Option<&'static str> {
26    ///         None
27    ///     }
28    /// }
29    /// ```
30    pub fn generate_trait_definition() -> String {
31        let mut code = String::new();
32
33        code.push_str("/// Trait for FHIR types that can be validated using invariants\n");
34        code.push_str("///\n");
35        code.push_str("/// This trait provides access to the invariants (constraints) defined\n");
36        code.push_str("/// in the FHIR specification for this resource or datatype.\n");
37        code.push_str("pub trait ValidatableResource {\n");
38        code.push_str("    /// Returns the FHIR resource type name\n");
39        code.push_str("    fn resource_type(&self) -> &'static str;\n");
40        code.push('\n');
41        code.push_str("    /// Returns the invariants (constraints) for this resource/datatype\n");
42        code.push_str("    ///\n");
43        code.push_str("    /// These are the FHIRPath expressions that must evaluate to true\n");
44        code.push_str("    /// for the resource to be considered valid.\n");
45        code.push_str("    fn invariants() -> &'static [rh_foundation::Invariant];\n");
46        code.push('\n');
47        code.push_str("    /// Returns the required bindings for this resource/datatype\n");
48        code.push_str("    ///\n");
49        code.push_str("    /// These are the ValueSet bindings with \"required\" strength that\n");
50        code.push_str("    /// must be validated at runtime.\n");
51        code.push_str("    fn bindings() -> &'static [rh_foundation::ElementBinding] {\n");
52        code.push_str("        &[]\n");
53        code.push_str("    }\n");
54        code.push('\n');
55        code.push_str("    /// Returns the cardinality constraints for this resource/datatype\n");
56        code.push_str("    ///\n");
57        code.push_str(
58            "    /// These define the minimum and maximum occurrences allowed for each element.\n",
59        );
60        code.push_str("    fn cardinalities() -> &'static [rh_foundation::ElementCardinality] {\n");
61        code.push_str("        &[]\n");
62        code.push_str("    }\n");
63        code.push('\n');
64        code.push_str("    /// Returns the profile URL if this is a profile, None otherwise\n");
65        code.push_str("    fn profile_url() -> Option<&'static str> {\n");
66        code.push_str("        None\n");
67        code.push_str("    }\n");
68        code.push_str("}\n");
69
70        code
71    }
72
73    /// Generate ValidatableResource trait implementation for a StructureDefinition
74    ///
75    /// Only generates an implementation if the type has invariants or bindings.
76    ///
77    /// Example output:
78    /// ```rust,ignore
79    /// impl ValidatableResource for Patient {
80    ///     fn resource_type(&self) -> &'static str {
81    ///         "Patient"
82    ///     }
83    ///     
84    ///     fn invariants() -> &'static [rh_foundation::Invariant] {
85    ///         &INVARIANTS
86    ///     }
87    ///     
88    ///     fn bindings() -> &'static [rh_foundation::ElementBinding] {
89    ///         &BINDINGS
90    ///     }
91    ///     
92    ///     fn profile_url() -> Option<&'static str> {
93    ///         Some("http://hl7.org/fhir/StructureDefinition/Patient")
94    ///     }
95    /// }
96    /// ```
97    pub fn generate_trait_impl(structure_def: &StructureDefinition) -> String {
98        let invariants = invariants::extract_invariants(structure_def);
99        let bindings = crate::bindings::extract_required_bindings(structure_def);
100
101        // Only generate implementation if there are invariants or bindings
102        if invariants.is_empty() && bindings.is_empty() {
103            return String::new();
104        }
105
106        let struct_name = crate::naming::Naming::struct_name(structure_def);
107        let resource_type = &structure_def.base_type;
108
109        let mut code = String::new();
110
111        code.push_str(&format!(
112            "impl crate::validation::ValidatableResource for {struct_name} {{\n"
113        ));
114        code.push_str("    fn resource_type(&self) -> &'static str {\n");
115        code.push_str(&format!("        \"{resource_type}\"\n"));
116        code.push_str("    }\n");
117        code.push('\n');
118        code.push_str("    fn invariants() -> &'static [rh_foundation::Invariant] {\n");
119        if invariants.is_empty() {
120            code.push_str("        &[]\n");
121        } else {
122            code.push_str("        &INVARIANTS\n");
123        }
124        code.push_str("    }\n");
125
126        // Add bindings() method if there are bindings
127        if !bindings.is_empty() {
128            code.push('\n');
129            code.push_str("    fn bindings() -> &'static [rh_foundation::ElementBinding] {\n");
130            code.push_str("        &BINDINGS\n");
131            code.push_str("    }\n");
132        }
133
134        // Add cardinalities() method - always include for resources/complex types
135        // We check if CARDINALITIES constant exists by looking at kind
136        if structure_def.kind == "resource" || structure_def.kind == "complex-type" {
137            code.push('\n');
138            code.push_str(
139                "    fn cardinalities() -> &'static [rh_foundation::ElementCardinality] {\n",
140            );
141            code.push_str("        &CARDINALITIES\n");
142            code.push_str("    }\n");
143        }
144
145        // Add profile_url if this is a constraint (profile)
146        // Profiles are identified by having kind="resource" and derivation="constraint"
147        // We can check if base_definition is present and is_abstract is false
148        if structure_def.base_definition.is_some() && !structure_def.is_abstract {
149            let url = &structure_def.url;
150            code.push('\n');
151            code.push_str("    fn profile_url() -> Option<&'static str> {\n");
152            code.push_str(&format!("        Some(\"{url}\")\n"));
153            code.push_str("    }\n");
154        }
155
156        code.push_str("}\n");
157
158        code
159    }
160
161    /// Generate the validation module file content
162    ///
163    /// This creates the validation.rs file that contains the ValidatableResource trait.
164    pub fn generate_validation_module() -> String {
165        let mut code = String::new();
166
167        code.push_str("//! Validation support for FHIR resources\n");
168        code.push_str("//!\n");
169        code.push_str(
170            "//! This module provides traits for validating FHIR resources using invariants.\n",
171        );
172        code.push('\n');
173        // Note: We use fully qualified `rh_foundation::Invariant` in the trait signature,
174        // so no import statement is needed.
175        code.push_str(&Self::generate_trait_definition());
176
177        code
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::fhir_types::{ElementConstraint, ElementDefinition, StructureDefinitionSnapshot};
185
186    fn create_test_structure_def(
187        name: &str,
188        base_type: &str,
189        constraints: Vec<ElementConstraint>,
190        url: String,
191        is_profile: bool,
192    ) -> StructureDefinition {
193        let element = ElementDefinition {
194            id: Some(base_type.to_string()),
195            path: base_type.to_string(),
196            element_type: None,
197            min: Some(0),
198            max: Some("*".to_string()),
199            short: None,
200            definition: None,
201            fixed: None,
202            pattern: None,
203            binding: None,
204            constraint: Some(constraints),
205        };
206
207        StructureDefinition {
208            resource_type: "StructureDefinition".to_string(),
209            id: name.to_string(),
210            url,
211            version: None,
212            name: name.to_string(),
213            title: None,
214            status: "active".to_string(),
215            description: None,
216            purpose: None,
217            kind: "resource".to_string(),
218            is_abstract: false,
219            base_type: base_type.to_string(),
220            base_definition: if is_profile {
221                Some(format!(
222                    "http://hl7.org/fhir/StructureDefinition/{base_type}"
223                ))
224            } else {
225                None
226            },
227            snapshot: Some(StructureDefinitionSnapshot {
228                element: vec![element],
229            }),
230            differential: None,
231        }
232    }
233
234    #[test]
235    fn test_generate_trait_definition() {
236        let trait_def = ValidationTraitGenerator::generate_trait_definition();
237
238        assert!(trait_def.contains("pub trait ValidatableResource"));
239        assert!(trait_def.contains("fn resource_type(&self) -> &'static str"));
240        assert!(trait_def.contains("fn invariants() -> &'static [rh_foundation::Invariant]"));
241        assert!(trait_def.contains("fn profile_url() -> Option<&'static str>"));
242        assert!(trait_def.contains("None"));
243    }
244
245    #[test]
246    fn test_generate_trait_impl_with_invariants() {
247        let constraints = vec![ElementConstraint {
248            key: "pat-1".to_string(),
249            severity: "error".to_string(),
250            human: "Test constraint".to_string(),
251            expression: Some("name.exists()".to_string()),
252            xpath: None,
253            source: None,
254        }];
255
256        let structure_def = create_test_structure_def(
257            "Patient",
258            "Patient",
259            constraints,
260            "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
261            false,
262        );
263
264        let impl_code = ValidationTraitGenerator::generate_trait_impl(&structure_def);
265
266        assert!(impl_code.contains("impl crate::validation::ValidatableResource for Patient"));
267        assert!(impl_code.contains("fn resource_type(&self) -> &'static str"));
268        assert!(impl_code.contains("\"Patient\""));
269        assert!(impl_code.contains("fn invariants() -> &'static [rh_foundation::Invariant]"));
270        assert!(impl_code.contains("&INVARIANTS"));
271    }
272
273    #[test]
274    fn test_generate_trait_impl_with_profile() {
275        let constraints = vec![ElementConstraint {
276            key: "prof-1".to_string(),
277            severity: "warning".to_string(),
278            human: "Profile constraint".to_string(),
279            expression: Some("identifier.exists()".to_string()),
280            xpath: None,
281            source: None,
282        }];
283
284        let structure_def = create_test_structure_def(
285            "USCorePatient",
286            "Patient",
287            constraints,
288            "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient".to_string(),
289            true,
290        );
291
292        let impl_code = ValidationTraitGenerator::generate_trait_impl(&structure_def);
293
294        assert!(impl_code.contains("fn profile_url() -> Option<&'static str>"));
295        assert!(impl_code
296            .contains("Some(\"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient\")"));
297    }
298
299    #[test]
300    fn test_generate_trait_impl_no_invariants() {
301        let structure_def = create_test_structure_def(
302            "Simple",
303            "Simple",
304            vec![],
305            "http://hl7.org/fhir/StructureDefinition/Simple".to_string(),
306            false,
307        );
308
309        let impl_code = ValidationTraitGenerator::generate_trait_impl(&structure_def);
310
311        assert_eq!(impl_code, "");
312    }
313
314    #[test]
315    fn test_generate_validation_module() {
316        let module_code = ValidationTraitGenerator::generate_validation_module();
317
318        assert!(module_code.contains("//! Validation support for FHIR resources"));
319        assert!(module_code.contains("pub trait ValidatableResource"));
320        assert!(module_code.contains("rh_foundation::Invariant")); // Used in trait signature
321        assert!(!module_code.contains("use rh_foundation::Invariant")); // No import needed
322    }
323
324    #[test]
325    fn test_trait_impl_base_resource() {
326        let constraints = vec![ElementConstraint {
327            key: "res-1".to_string(),
328            severity: "error".to_string(),
329            human: "Resource constraint".to_string(),
330            expression: Some("id.exists()".to_string()),
331            xpath: None,
332            source: None,
333        }];
334
335        let structure_def = create_test_structure_def(
336            "Observation",
337            "Observation",
338            constraints,
339            "http://hl7.org/fhir/StructureDefinition/Observation".to_string(),
340            false,
341        );
342
343        let impl_code = ValidationTraitGenerator::generate_trait_impl(&structure_def);
344
345        // Should not have profile_url for base resources (not a profile)
346        assert!(!impl_code.contains("fn profile_url()"));
347        assert!(impl_code.contains("impl crate::validation::ValidatableResource for Observation"));
348    }
349}