rh_codegen/generators/trait_impl_generator/
mod.rs1mod 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
15pub struct TraitImplGenerator;
17
18#[allow(dead_code)]
19impl TraitImplGenerator {
20 pub fn new() -> Self {
22 Self
23 }
24
25 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 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 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 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 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 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 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 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 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 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 #[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 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 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 #[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 #[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 #[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 #[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 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 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}