Skip to main content

rh_codegen/generators/trait_impl_generator/
mod.rs

1//! Trait implementation generation functionality
2//!
3//! This module handles the generation of trait implementations for FHIR resources.
4
5mod accessor_impl;
6mod existence_impl;
7mod mutator_impl;
8
9use super::utils::GeneratorUtils;
10use crate::fhir_types::StructureDefinition;
11use crate::naming::Naming;
12use crate::rust_types::RustTraitImpl;
13use crate::CodegenResult;
14
15/// Generator for trait implementations
16pub struct TraitImplGenerator;
17
18#[allow(dead_code)]
19impl TraitImplGenerator {
20    /// Create a new trait implementation generator
21    pub fn new() -> Self {
22        Self
23    }
24
25    /// Extract the base resource type from a FHIR baseDefinition URL
26    /// For example: "http://hl7.org/fhir/StructureDefinition/Group" -> "Group"
27    pub(crate) fn extract_base_resource_type(base_definition: &str) -> Option<String> {
28        if base_definition.starts_with("http://hl7.org/fhir/StructureDefinition/") {
29            if let Some(last_segment) = base_definition.split('/').next_back() {
30                return Some(last_segment.to_string());
31            }
32        }
33        None
34    }
35
36    /// Check if a baseDefinition indicates this is a core FHIR resource
37    /// Core resources inherit directly from Resource or DomainResource
38    pub(crate) fn is_core_resource(base_definition: &str) -> bool {
39        matches!(
40            base_definition,
41            "http://hl7.org/fhir/StructureDefinition/Resource"
42                | "http://hl7.org/fhir/StructureDefinition/DomainResource"
43        )
44    }
45
46    /// Get the core resource type that a profile ultimately inherits from
47    /// This handles known profile inheritance chains and can be extended with dynamic loading
48    pub(crate) fn resolve_to_core_resource_type(
49        base_resource_type: &str,
50        _base_definition_url: &str,
51    ) -> String {
52        match base_resource_type.to_lowercase().as_str() {
53            "vitalsigns" => "Observation".to_string(),
54            "bodyweight" | "bodyheight" | "bmi" | "bodytemp" | "heartrate" | "resprate"
55            | "oxygensat" => "Observation".to_string(),
56            _ => base_resource_type.to_string(),
57        }
58    }
59
60    /// Get the appropriate resource type for a structure definition
61    /// For profiles, returns the base resource type; for core resources, returns the struct name
62    pub(crate) fn get_resource_type_for_struct(
63        struct_name: &str,
64        structure_def: &StructureDefinition,
65    ) -> String {
66        if let Some(base_def) = &structure_def.base_definition {
67            if Self::is_core_resource(base_def) {
68                return struct_name.to_string();
69            }
70
71            if let Some(base_resource_type) = Self::extract_base_resource_type(base_def) {
72                let core_resource_type =
73                    Self::resolve_to_core_resource_type(&base_resource_type, base_def);
74
75                if GeneratorUtils::is_fhir_resource_type(&core_resource_type) {
76                    return core_resource_type;
77                }
78            }
79        }
80
81        struct_name.to_string()
82    }
83
84    /// Generate trait implementations for a FHIR resource
85    pub fn generate_trait_impls(
86        &self,
87        structure_def: &StructureDefinition,
88    ) -> CodegenResult<Vec<RustTraitImpl>> {
89        let mut trait_impls = Vec::new();
90
91        if structure_def.kind != "resource" {
92            return Ok(trait_impls);
93        }
94
95        let struct_name = Naming::struct_name(structure_def);
96
97        trait_impls.push(self.generate_resource_trait_impl(&struct_name, structure_def));
98        trait_impls.push(self.generate_resource_mutators_trait_impl(&struct_name, structure_def));
99        trait_impls.push(self.generate_resource_existence_trait_impl(&struct_name, structure_def));
100
101        if let Some(base_def) = &structure_def.base_definition {
102            if base_def.contains("DomainResource") {
103                trait_impls.push(self.generate_domain_resource_trait_impl(&struct_name));
104                trait_impls.push(self.generate_domain_resource_mutators_trait_impl(&struct_name));
105                trait_impls.push(self.generate_domain_resource_existence_trait_impl(&struct_name));
106            }
107        }
108
109        if struct_name != "Resource" {
110            let specific_trait_impl =
111                self.generate_specific_resource_trait_impl(&struct_name, structure_def);
112
113            if !specific_trait_impl.is_empty() {
114                trait_impls.push(specific_trait_impl);
115            }
116
117            let specific_mutators_trait_impl =
118                self.generate_specific_resource_mutators_trait_impl(&struct_name, structure_def);
119
120            if !specific_mutators_trait_impl.is_empty() {
121                trait_impls.push(specific_mutators_trait_impl);
122            }
123
124            let specific_existence_trait_impl =
125                self.generate_specific_resource_existence_trait_impl(&struct_name, structure_def);
126
127            if !specific_existence_trait_impl.is_empty() {
128                trait_impls.push(specific_existence_trait_impl);
129            }
130        }
131
132        Ok(trait_impls)
133    }
134
135    /// Get the base access pattern for resource fields
136    pub(crate) fn get_resource_base_access(
137        &self,
138        struct_name: &str,
139        structure_def: &StructureDefinition,
140    ) -> (String, bool) {
141        if struct_name == "Resource" {
142            ("self".to_string(), false)
143        } else if struct_name == "DomainResource" {
144            ("self.base".to_string(), false)
145        } else if let Some(base_def) = &structure_def.base_definition {
146            if base_def.contains("DomainResource") {
147                ("self.base.base".to_string(), false)
148            } else if base_def.contains("Resource") && struct_name != "DomainResource" {
149                ("self.base".to_string(), false)
150            } else if base_def.starts_with("http://hl7.org/fhir/StructureDefinition/") {
151                ("self.base".to_string(), true)
152            } else {
153                ("self.base.base".to_string(), false)
154            }
155        } else {
156            ("self.base.base".to_string(), false)
157        }
158    }
159
160    /// Check if we should generate an accessor implementation for this element
161    pub(crate) fn should_generate_accessor_impl(
162        &self,
163        element: &crate::fhir_types::ElementDefinition,
164        structure_def: &StructureDefinition,
165    ) -> bool {
166        let field_path = &element.path;
167        let base_name = &structure_def.name;
168
169        if !field_path.starts_with(base_name) {
170            return false;
171        }
172
173        let path_parts: Vec<&str> = field_path.split('.').collect();
174        if path_parts.len() != 2 {
175            return false;
176        }
177
178        if path_parts[0] != base_name {
179            return false;
180        }
181
182        let field_name = path_parts[1];
183        !field_name.ends_with("[x]")
184    }
185
186    /// Check if the element types contain BackboneElement
187    pub(crate) fn is_backbone_element(
188        &self,
189        element_types: &[crate::fhir_types::ElementType],
190    ) -> bool {
191        element_types
192            .iter()
193            .any(|et| et.code.as_deref() == Some("BackboneElement"))
194    }
195
196    /// Get the specific nested type for a BackboneElement field
197    pub(crate) fn get_nested_type_for_backbone_element(
198        &self,
199        element: &crate::fhir_types::ElementDefinition,
200        is_array: bool,
201    ) -> crate::rust_types::RustType {
202        let path_parts: Vec<&str> = element.path.split('.').collect();
203
204        if path_parts.len() == 2 {
205            let resource_name = path_parts[0];
206            let field_name = path_parts[1];
207
208            let field_name_pascal = crate::naming::Naming::to_pascal_case(field_name);
209            let nested_type_name = format!("{resource_name}{field_name_pascal}");
210
211            let rust_type = crate::rust_types::RustType::Custom(nested_type_name);
212
213            if is_array {
214                crate::rust_types::RustType::Vec(Box::new(rust_type))
215            } else {
216                rust_type
217            }
218        } else {
219            let rust_type = crate::rust_types::RustType::Custom("BackboneElement".to_string());
220            if is_array {
221                crate::rust_types::RustType::Vec(Box::new(rust_type))
222            } else {
223                rust_type
224            }
225        }
226    }
227
228    /// Get the inner type for slice return type
229    pub(crate) fn get_inner_type_for_slice(
230        &self,
231        rust_type: &crate::rust_types::RustType,
232    ) -> String {
233        match rust_type {
234            crate::rust_types::RustType::Vec(inner) => inner.to_string(),
235            crate::rust_types::RustType::Option(inner) => {
236                if let crate::rust_types::RustType::Vec(vec_inner) = inner.as_ref() {
237                    vec_inner.to_string()
238                } else {
239                    inner.to_string()
240                }
241            }
242            _ => rust_type.to_string(),
243        }
244    }
245
246    /// Get the type for option return type
247    #[allow(dead_code)]
248    pub(crate) fn get_type_for_option(&self, rust_type: &crate::rust_types::RustType) -> String {
249        match rust_type {
250            crate::rust_types::RustType::Option(inner) => inner.to_string(),
251            _ => rust_type.to_string(),
252        }
253    }
254
255    /// Check if a RustType implements the Copy trait
256    pub(crate) fn is_copy_type(&self, rust_type: &crate::rust_types::RustType) -> bool {
257        match rust_type {
258            crate::rust_types::RustType::Boolean
259            | crate::rust_types::RustType::Integer
260            | crate::rust_types::RustType::Float => true,
261
262            crate::rust_types::RustType::Option(inner) => self.is_copy_type(inner),
263
264            crate::rust_types::RustType::Custom(type_name) => {
265                self.is_copy_primitive_type(type_name)
266            }
267
268            _ => false,
269        }
270    }
271
272    /// Check if a custom type name represents a FHIR primitive type that is Copy
273    pub(crate) fn is_copy_primitive_type(&self, type_name: &str) -> bool {
274        matches!(
275            type_name,
276            "BooleanType" | "IntegerType" | "UnsignedIntType" | "PositiveIntType" | "DecimalType"
277        )
278    }
279
280    /// Check if a rust type represents an enum
281    #[allow(dead_code)]
282    pub(crate) fn is_enum_type(&self, rust_type: &crate::rust_types::RustType) -> bool {
283        match rust_type {
284            crate::rust_types::RustType::Custom(type_name) => self.is_enum_type_name(type_name),
285            _ => false,
286        }
287    }
288
289    /// Check if a type name represents a FHIR enum type
290    #[allow(dead_code)]
291    pub(crate) fn is_enum_type_name(&self, type_name: &str) -> bool {
292        type_name.ends_with("Status")
293            || type_name.ends_with("Kind")
294            || type_name.ends_with("Code")
295            || type_name.ends_with("Codes")
296            || type_name.ends_with("Priority")
297            || type_name.ends_with("Intent")
298            || matches!(
299                type_name,
300                "PublicationStatus"
301                    | "CapabilityStatementKind"
302                    | "CodeSearchSupport"
303                    | "FmStatus"
304                    | "ReportStatusCodes"
305                    | "ReportResultCodes"
306                    | "VerificationresultStatus"
307                    | "TaskStatus"
308                    | "TaskIntent"
309                    | "RequestPriority"
310                    | "SupplydeliveryStatus"
311                    | "SupplyrequestStatus"
312            )
313    }
314
315    /// Determine the return type for a field accessor method
316    #[allow(dead_code)]
317    pub(crate) fn determine_method_return_type(
318        &self,
319        element: &crate::fhir_types::ElementDefinition,
320    ) -> String {
321        let is_optional = element.min.unwrap_or(0) == 0;
322
323        let is_array = element
324            .max
325            .as_ref()
326            .is_some_and(|max| max == "*" || max.parse::<u32>().unwrap_or(1) > 1);
327
328        let base_type = if let Some(element_types) = &element.element_type {
329            if let Some(first_type) = element_types.first() {
330                if let Some(code) = &first_type.code {
331                    match code.as_str() {
332                        "string" | "code" | "id" | "markdown" | "uri" | "url" | "canonical"
333                        | "dateTime" | "date" | "time" | "instant" | "base64Binary" | "oid"
334                        | "uuid" => "String".to_string(),
335                        "boolean" => "bool".to_string(),
336                        "integer" | "positiveInt" | "unsignedInt" => "i32".to_string(),
337                        "decimal" => "f64".to_string(),
338                        "Reference" => "crate::datatypes::reference::Reference".to_string(),
339                        "Identifier" => "crate::datatypes::identifier::Identifier".to_string(),
340                        "CodeableConcept" => {
341                            "crate::datatypes::codeable_concept::CodeableConcept".to_string()
342                        }
343                        "Coding" => "crate::datatypes::coding::Coding".to_string(),
344                        "Address" => "crate::datatypes::address::Address".to_string(),
345                        "HumanName" => "crate::datatypes::human_name::HumanName".to_string(),
346                        "ContactPoint" => {
347                            "crate::datatypes::contact_point::ContactPoint".to_string()
348                        }
349                        "Attachment" => "crate::datatypes::attachment::Attachment".to_string(),
350                        "Annotation" => "crate::datatypes::annotation::Annotation".to_string(),
351                        "BackboneElement" => {
352                            "crate::datatypes::backbone_element::BackboneElement".to_string()
353                        }
354                        _ => "String".to_string(),
355                    }
356                } else {
357                    "String".to_string()
358                }
359            } else {
360                "String".to_string()
361            }
362        } else {
363            "String".to_string()
364        };
365
366        if is_array {
367            format!("Vec<{base_type}>")
368        } else if is_optional {
369            format!("Option<{base_type}>")
370        } else {
371            base_type
372        }
373    }
374
375    /// Generate the method body for a field accessor
376    #[allow(dead_code)]
377    pub(crate) fn generate_method_body(
378        &self,
379        field_name: &str,
380        element: &crate::fhir_types::ElementDefinition,
381    ) -> String {
382        let rust_field_name = if field_name == "type" {
383            "type_".to_string()
384        } else {
385            crate::naming::Naming::field_name(field_name)
386        };
387
388        let field_access = format!("self.{rust_field_name}");
389
390        let is_optional = element.min.unwrap_or(0) == 0;
391        let is_array = element
392            .max
393            .as_ref()
394            .is_some_and(|max| max == "*" || max.parse::<u32>().unwrap_or(1) > 1);
395
396        if is_array {
397            format!("{field_access}.clone()")
398        } else if let Some(type_def) = element
399            .element_type
400            .as_ref()
401            .and_then(|types| types.first())
402        {
403            if let Some(code) = &type_def.code {
404                match code.as_str() {
405                    "string" | "code" | "id" | "markdown" | "uri" | "url" | "canonical"
406                    | "dateTime" | "date" | "time" | "instant" | "base64Binary" | "oid"
407                    | "uuid" => {
408                        if is_optional {
409                            format!("{field_access}.as_ref().map(|s| s.to_string())")
410                        } else {
411                            format!("{field_access}.to_string()")
412                        }
413                    }
414                    "boolean" => {
415                        if is_optional {
416                            format!("{field_access}.map(|b| b.into())")
417                        } else {
418                            format!("{field_access}.into()")
419                        }
420                    }
421                    "integer" | "positiveInt" | "unsignedInt" => {
422                        if is_optional {
423                            format!("{field_access}.map(|i| i.into())")
424                        } else {
425                            format!("{field_access}.into()")
426                        }
427                    }
428                    "decimal" => {
429                        if is_optional {
430                            format!("{field_access}.map(|d| d.into())")
431                        } else {
432                            format!("{field_access}.into()")
433                        }
434                    }
435                    "CodeableConcept" | "Reference" | "Identifier" | "Coding" | "Address"
436                    | "HumanName" | "ContactPoint" | "Attachment" | "Annotation"
437                    | "BackboneElement" => {
438                        format!("{field_access}.clone()")
439                    }
440                    _ => {
441                        if is_optional {
442                            format!("{field_access}.as_ref().map(|v| format!(\"{{:?}}\", v))")
443                        } else {
444                            format!("format!(\"{{:?}}\", {field_access})")
445                        }
446                    }
447                }
448            } else {
449                format!("{field_access}.clone()")
450            }
451        } else {
452            format!("{field_access}.clone()")
453        }
454    }
455
456    /// Get the Rust type for a field element, considering ValueSet bindings.
457    pub(crate) fn get_field_rust_type(
458        &self,
459        element: &crate::fhir_types::ElementDefinition,
460        field_name: &str,
461    ) -> CodegenResult<crate::rust_types::RustType> {
462        use crate::rust_types::RustType;
463
464        let Some(element_type) = element.element_type.as_ref().and_then(|t| t.first()) else {
465            return Ok(RustType::String);
466        };
467
468        let Some(code) = &element_type.code else {
469            return Ok(RustType::String);
470        };
471
472        if code == "code" {
473            if let Some(binding) = &element.binding {
474                if binding.strength == "required" {
475                    if let Some(value_set_url) = &binding.value_set {
476                        if let Some(enum_name) =
477                            self.extract_enum_name_from_value_set(value_set_url)
478                        {
479                            let resource_name = element.path.split('.').next().unwrap_or("");
480                            if enum_name != resource_name {
481                                return Ok(RustType::Custom(enum_name));
482                            }
483                        }
484                    }
485                }
486            }
487        }
488
489        use crate::generators::TypeUtilities;
490        TypeUtilities::map_fhir_type_to_rust(element_type, field_name, &element.path)
491    }
492
493    /// Extract enum type name from a ValueSet URL
494    pub(crate) fn extract_enum_name_from_value_set(&self, url: &str) -> Option<String> {
495        let url_without_version = url.split('|').next().unwrap_or(url);
496
497        let value_set_name = url_without_version.split('/').next_back()?;
498
499        let name = value_set_name
500            .split(&['-', '.'][..])
501            .filter(|part| !part.is_empty())
502            .map(|part| {
503                let mut chars = part.chars();
504                match chars.next() {
505                    None => String::new(),
506                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
507                }
508            })
509            .collect::<String>();
510
511        if name.chars().next().unwrap_or('0').is_ascii_digit() {
512            Some(format!("ValueSet{name}"))
513        } else {
514            Some(name)
515        }
516    }
517}
518
519impl Default for TraitImplGenerator {
520    fn default() -> Self {
521        Self::new()
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528    use crate::fhir_types::StructureDefinitionDifferential;
529
530    fn create_test_structure_definition(
531        name: &str,
532        base_definition: Option<&str>,
533    ) -> StructureDefinition {
534        StructureDefinition {
535            resource_type: "StructureDefinition".to_string(),
536            id: name.to_lowercase(),
537            url: format!("http://test.com/{name}"),
538            version: Some("1.0.0".to_string()),
539            name: name.to_string(),
540            title: Some(name.to_string()),
541            status: "active".to_string(),
542            description: None,
543            purpose: None,
544            kind: "resource".to_string(),
545            is_abstract: false,
546            base_type: "Resource".to_string(),
547            base_definition: base_definition.map(|s| s.to_string()),
548            differential: None,
549            snapshot: None,
550        }
551    }
552
553    #[test]
554    fn test_resource_type_for_core_resource() {
555        let patient = create_test_structure_definition(
556            "Patient",
557            Some("http://hl7.org/fhir/StructureDefinition/DomainResource"),
558        );
559
560        let result = TraitImplGenerator::get_resource_type_for_struct("Patient", &patient);
561        assert_eq!(
562            result, "Patient",
563            "Core resource should return its own name"
564        );
565    }
566
567    #[test]
568    fn test_resource_type_for_group_profile() {
569        let group_definition = create_test_structure_definition(
570            "GroupDefinition",
571            Some("http://hl7.org/fhir/StructureDefinition/Group"),
572        );
573
574        let result =
575            TraitImplGenerator::get_resource_type_for_struct("GroupDefinition", &group_definition);
576        assert_eq!(result, "Group", "Group profile should return 'Group'");
577    }
578
579    #[test]
580    fn test_resource_type_for_observation_profile() {
581        let vital_signs = create_test_structure_definition(
582            "VitalSigns",
583            Some("http://hl7.org/fhir/StructureDefinition/Observation"),
584        );
585
586        let result = TraitImplGenerator::get_resource_type_for_struct("VitalSigns", &vital_signs);
587        assert_eq!(
588            result, "Observation",
589            "Observation profile should return 'Observation'"
590        );
591    }
592
593    #[test]
594    fn test_resource_type_for_profile_on_profile() {
595        let bmi = create_test_structure_definition(
596            "BMI",
597            Some("http://hl7.org/fhir/StructureDefinition/vitalsigns"),
598        );
599
600        let result = TraitImplGenerator::get_resource_type_for_struct("BMI", &bmi);
601        assert_eq!(
602            result, "Observation",
603            "BMI profile should resolve to 'Observation' via vitalsigns"
604        );
605    }
606
607    #[test]
608    fn test_resource_type_without_base_definition() {
609        let custom_resource = create_test_structure_definition("CustomResource", None);
610
611        let result =
612            TraitImplGenerator::get_resource_type_for_struct("CustomResource", &custom_resource);
613        assert_eq!(
614            result, "CustomResource",
615            "Resource without baseDefinition should return struct name"
616        );
617    }
618
619    #[test]
620    fn test_is_core_resource() {
621        assert!(TraitImplGenerator::is_core_resource(
622            "http://hl7.org/fhir/StructureDefinition/Resource"
623        ));
624        assert!(TraitImplGenerator::is_core_resource(
625            "http://hl7.org/fhir/StructureDefinition/DomainResource"
626        ));
627        assert!(!TraitImplGenerator::is_core_resource(
628            "http://hl7.org/fhir/StructureDefinition/Patient"
629        ));
630        assert!(!TraitImplGenerator::is_core_resource(
631            "http://hl7.org/fhir/StructureDefinition/Group"
632        ));
633    }
634
635    #[test]
636    fn test_extract_base_resource_type() {
637        assert_eq!(
638            TraitImplGenerator::extract_base_resource_type(
639                "http://hl7.org/fhir/StructureDefinition/Group"
640            ),
641            Some("Group".to_string())
642        );
643        assert_eq!(
644            TraitImplGenerator::extract_base_resource_type(
645                "http://hl7.org/fhir/StructureDefinition/Observation"
646            ),
647            Some("Observation".to_string())
648        );
649        assert_eq!(
650            TraitImplGenerator::extract_base_resource_type(
651                "http://hl7.org/fhir/StructureDefinition/vitalsigns"
652            ),
653            Some("vitalsigns".to_string())
654        );
655        assert_eq!(
656            TraitImplGenerator::extract_base_resource_type("invalid-url"),
657            None
658        );
659    }
660
661    #[test]
662    fn test_resolve_to_core_resource_type() {
663        assert_eq!(
664            TraitImplGenerator::resolve_to_core_resource_type(
665                "vitalsigns",
666                "http://hl7.org/fhir/StructureDefinition/vitalsigns"
667            ),
668            "Observation"
669        );
670
671        assert_eq!(
672            TraitImplGenerator::resolve_to_core_resource_type(
673                "Patient",
674                "http://hl7.org/fhir/StructureDefinition/Patient"
675            ),
676            "Patient"
677        );
678        assert_eq!(
679            TraitImplGenerator::resolve_to_core_resource_type(
680                "Group",
681                "http://hl7.org/fhir/StructureDefinition/Group"
682            ),
683            "Group"
684        );
685
686        assert_eq!(
687            TraitImplGenerator::resolve_to_core_resource_type(
688                "bmi",
689                "http://hl7.org/fhir/StructureDefinition/bmi"
690            ),
691            "Observation"
692        );
693
694        assert_eq!(
695            TraitImplGenerator::resolve_to_core_resource_type(
696                "UnknownProfile",
697                "http://hl7.org/fhir/StructureDefinition/UnknownProfile"
698            ),
699            "UnknownProfile"
700        );
701    }
702
703    #[test]
704    fn test_empty_trait_implementations_are_filtered() {
705        let generator = TraitImplGenerator::new();
706
707        let mut structure_def = create_test_structure_definition("EmptyProfile", None);
708        structure_def.differential = Some(StructureDefinitionDifferential { element: vec![] });
709
710        let trait_impls = generator.generate_trait_impls(&structure_def).unwrap();
711
712        assert!(
713            !trait_impls.is_empty(),
714            "Should have at least Resource trait impl"
715        );
716
717        let specific_trait_name = format!(
718            "crate::traits::{}::{}Accessors",
719            crate::naming::Naming::to_snake_case("EmptyProfile"),
720            "EmptyProfile"
721        );
722
723        let has_empty_specific_impl = trait_impls
724            .iter()
725            .any(|impl_| impl_.trait_name == specific_trait_name && impl_.is_empty());
726
727        assert!(
728            !has_empty_specific_impl,
729            "Should not include empty specific trait implementations"
730        );
731    }
732}