1use once_cell::sync::Lazy;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct FhirResourceInfo {
12 pub canonical_name: String,
14 pub module_name: String,
16 pub is_core_resource: bool,
18}
19
20impl FhirResourceInfo {
21 fn new(canonical_name: &str) -> Self {
22 Self {
23 canonical_name: canonical_name.to_string(),
24 module_name: crate::naming::Naming::to_snake_case(canonical_name),
25 is_core_resource: true,
26 }
27 }
28}
29
30static FHIR_RESOURCE_MAP: Lazy<HashMap<String, FhirResourceInfo>> = Lazy::new(|| {
32 let resource_names = [
33 "Account",
34 "ActivityDefinition",
35 "AdverseEvent",
36 "AllergyIntolerance",
37 "Appointment",
38 "AppointmentResponse",
39 "AuditEvent",
40 "Basic",
41 "Binary",
42 "BiologicallyDerivedProduct",
43 "BodyStructure",
44 "Bundle",
45 "CapabilityStatement",
46 "CarePlan",
47 "CareTeam",
48 "CatalogEntry",
49 "ChargeItem",
50 "ChargeItemDefinition",
51 "Claim",
52 "ClaimResponse",
53 "ClinicalImpression",
54 "CodeSystem",
55 "Communication",
56 "CommunicationRequest",
57 "CompartmentDefinition",
58 "Composition",
59 "ConceptMap",
60 "Condition",
61 "Consent",
62 "Contract",
63 "Coverage",
64 "CoverageEligibilityRequest",
65 "CoverageEligibilityResponse",
66 "DetectedIssue",
67 "Device",
68 "DeviceDefinition",
69 "DeviceMetric",
70 "DeviceRequest",
71 "DeviceUseStatement",
72 "DiagnosticReport",
73 "DocumentManifest",
74 "DocumentReference",
75 "EffectEvidenceSynthesis",
76 "Encounter",
77 "Endpoint",
78 "EnrollmentRequest",
79 "EnrollmentResponse",
80 "EpisodeOfCare",
81 "EventDefinition",
82 "Evidence",
83 "EvidenceVariable",
84 "ExampleScenario",
85 "ExplanationOfBenefit",
86 "FamilyMemberHistory",
87 "Flag",
88 "Goal",
89 "GraphDefinition",
90 "Group",
91 "GuidanceResponse",
92 "HealthcareService",
93 "ImagingStudy",
94 "Immunization",
95 "ImmunizationEvaluation",
96 "ImmunizationRecommendation",
97 "ImplementationGuide",
98 "InsurancePlan",
99 "Invoice",
100 "Library",
101 "Linkage",
102 "List",
103 "Location",
104 "Measure",
105 "MeasureReport",
106 "Media",
107 "Medication",
108 "MedicationAdministration",
109 "MedicationDispense",
110 "MedicationKnowledge",
111 "MedicationRequest",
112 "MedicationStatement",
113 "MedicinalProduct",
114 "MedicinalProductAuthorization",
115 "MedicinalProductContraindication",
116 "MedicinalProductIndication",
117 "MedicinalProductIngredient",
118 "MedicinalProductInteraction",
119 "MedicinalProductManufactured",
120 "MedicinalProductPackaged",
121 "MedicinalProductPharmaceutical",
122 "MedicinalProductUndesirableEffect",
123 "MessageDefinition",
124 "MessageHeader",
125 "MolecularSequence",
126 "NamingSystem",
127 "NutritionOrder",
128 "Observation",
129 "ObservationDefinition",
130 "OperationDefinition",
131 "OperationOutcome",
132 "Organization",
133 "OrganizationAffiliation",
134 "Patient",
135 "Parameters",
136 "PaymentNotice",
137 "PaymentReconciliation",
138 "Person",
139 "PlanDefinition",
140 "Practitioner",
141 "PractitionerRole",
142 "Procedure",
143 "Provenance",
144 "Questionnaire",
145 "QuestionnaireResponse",
146 "RelatedPerson",
147 "RequestGroup",
148 "ResearchDefinition",
149 "ResearchElementDefinition",
150 "ResearchStudy",
151 "ResearchSubject",
152 "RiskAssessment",
153 "RiskEvidenceSynthesis",
154 "Schedule",
155 "SearchParameter",
156 "ServiceRequest",
157 "Slot",
158 "Specimen",
159 "SpecimenDefinition",
160 "StructureDefinition",
161 "StructureMap",
162 "Subscription",
163 "Substance",
164 "SubstanceNucleicAcid",
165 "SubstancePolymer",
166 "SubstanceProtein",
167 "SubstanceReferenceInformation",
168 "SubstanceSourceMaterial",
169 "SubstanceSpecification",
170 "SupplyDelivery",
171 "SupplyRequest",
172 "Task",
173 "TerminologyCapabilities",
174 "TestReport",
175 "TestScript",
176 "ValueSet",
177 "VerificationResult",
178 "VisionPrescription",
179 "DomainResource",
181 "Resource",
182 "MetadataResource",
183 ];
184
185 let mut map = HashMap::new();
186 for name in &resource_names {
187 map.insert(name.to_string(), FhirResourceInfo::new(name));
188 }
189 map
190});
191
192static FHIR_DATATYPE_MAP: Lazy<HashMap<String, FhirResourceInfo>> = Lazy::new(|| {
194 let datatype_names = [
195 "Identifier",
196 "HumanName",
197 "Address",
198 "ContactPoint",
199 "Attachment",
200 "CodeableConcept",
201 "Coding",
202 "Quantity",
203 "Range",
204 "Period",
205 "Ratio",
206 "SampledData",
207 "Signature",
208 "Age",
209 "Count",
210 "Distance",
211 "Duration",
212 "Money",
213 "SimpleQuantity",
214 "Extension",
215 "Narrative",
216 "Annotation",
217 "Reference",
218 "Meta",
219 "ContactDetail",
220 "Contributor",
221 "DataRequirement",
222 "Expression",
223 "ParameterDefinition",
224 "RelatedArtifact",
225 "TriggerDefinition",
226 "UsageContext",
227 "Dosage",
228 "Population",
229 "ProductShelfLife",
230 "ProdCharacteristic",
231 "MarketingStatus",
232 "SubstanceAmount",
233 "Timing",
234 "ElementDefinition",
235 "Element",
236 "BackboneElement",
237 ];
238
239 let mut map = HashMap::new();
240 for name in &datatype_names {
241 let info = FhirResourceInfo {
242 canonical_name: name.to_string(),
243 module_name: crate::naming::Naming::to_snake_case(name),
244 is_core_resource: false,
245 };
246 map.insert(name.to_string(), info);
247 }
248 map
249});
250
251pub struct NamingManager;
253
254impl NamingManager {
255 pub fn get_resource_info(resource_name: &str) -> Option<&FhirResourceInfo> {
257 FHIR_RESOURCE_MAP.get(resource_name)
258 }
259
260 pub fn get_datatype_info(datatype_name: &str) -> Option<&FhirResourceInfo> {
262 FHIR_DATATYPE_MAP.get(datatype_name)
263 }
264
265 pub fn is_fhir_resource(type_name: &str) -> bool {
267 FHIR_RESOURCE_MAP.contains_key(type_name)
268 }
269
270 pub fn is_fhir_datatype(type_name: &str) -> bool {
272 FHIR_DATATYPE_MAP.contains_key(type_name)
273 }
274
275 pub fn detect_nested_structure_parent(type_name: &str) -> Option<String> {
283 let mut longest_match = None;
284 let mut longest_length = 0;
285
286 for resource_name in FHIR_RESOURCE_MAP.keys() {
288 if type_name.starts_with(resource_name)
289 && type_name.len() > resource_name.len()
290 && resource_name.len() > longest_length
291 {
292 if let Some(next_char) = type_name.chars().nth(resource_name.len()) {
294 if next_char.is_uppercase() {
295 longest_match = Some(resource_name.clone());
296 longest_length = resource_name.len();
297 }
298 }
299 }
300 }
301
302 longest_match
303 }
304
305 pub fn get_import_path_for_type(type_name: &str) -> String {
307 if Self::is_generated_trait(type_name) {
309 return Self::get_trait_import_path(type_name);
310 }
311
312 if Self::is_likely_binding_enum(type_name) {
314 return format!(
315 "crate::bindings::{}::{}",
316 crate::naming::Naming::to_snake_case(type_name),
317 type_name
318 );
319 }
320
321 if let Some(parent_resource) = Self::detect_nested_structure_parent(type_name) {
323 if let Some(resource_info) = Self::get_resource_info(&parent_resource) {
324 return format!(
325 "crate::resources::{}::{}",
326 resource_info.module_name, type_name
327 );
328 }
329 }
330
331 if let Some(resource_info) = Self::get_resource_info(type_name) {
333 return format!(
334 "crate::resources::{}::{}",
335 resource_info.module_name, type_name
336 );
337 }
338
339 if let Some(datatype_info) = Self::get_datatype_info(type_name) {
341 return format!(
342 "crate::datatypes::{}::{}",
343 datatype_info.module_name, type_name
344 );
345 }
346
347 if let Some(primitive_path) = Self::get_primitive_import_path(type_name) {
349 return primitive_path;
350 }
351
352 format!(
354 "crate::bindings::{}::{}",
355 crate::naming::Naming::to_snake_case(type_name),
356 type_name
357 )
358 }
359
360 fn get_primitive_import_path(type_name: &str) -> Option<String> {
362 let module_name = match type_name {
363 "StringType" | "String" => "string",
364 "BooleanType" | "Boolean" => "boolean",
365 "IntegerType" | "Integer" => "integer",
366 "DecimalType" | "Decimal" => "decimal",
367 "UriType" | "Uri" => "uri",
368 "UrlType" | "Url" => "url",
369 "CanonicalType" | "Canonical" => "canonical",
370 "OidType" | "Oid" => "oid",
371 "UuidType" | "Uuid" => "uuid",
372 "InstantType" | "Instant" => "instant",
373 "DateType" | "Date" => "date",
374 "DateTimeType" | "DateTime" => "date_time",
375 "TimeType" | "Time" => "time",
376 "CodeType" | "Code" => "code",
377 "IdType" | "Id" => "id",
378 "MarkdownType" | "Markdown" => "markdown",
379 "Base64BinaryType" | "Base64Binary" => "base64binary",
380 "UnsignedIntType" | "UnsignedInt" => "unsigned_int",
381 "PositiveIntType" | "PositiveInt" => "positive_int",
382 "XhtmlType" | "Xhtml" => "xhtml",
383 _ => return None,
384 };
385
386 let type_variant = match type_name {
388 "String" => "StringType",
389 "Boolean" => "BooleanType",
390 "Integer" => "IntegerType",
391 "Decimal" => "DecimalType",
392 "Uri" => "UriType",
393 "Url" => "UrlType",
394 "Canonical" => "CanonicalType",
395 "Oid" => "OidType",
396 "Uuid" => "UuidType",
397 "Instant" => "InstantType",
398 "Date" => "DateType",
399 "DateTime" => "DateTimeType",
400 "Time" => "TimeType",
401 "Code" => "CodeType",
402 "Id" => "IdType",
403 "Markdown" => "MarkdownType",
404 "Base64Binary" => "Base64BinaryType",
405 "UnsignedInt" => "UnsignedIntType",
406 "PositiveInt" => "PositiveIntType",
407 "Xhtml" => "XhtmlType",
408 _ => type_name, };
410
411 Some(format!("crate::primitives::{module_name}::{type_variant}"))
412 }
413
414 fn is_generated_trait(type_name: &str) -> bool {
416 type_name.ends_with("Accessors")
417 || type_name.ends_with("Mutators")
418 || type_name.ends_with("Helpers")
419 }
420
421 fn is_likely_binding_enum(type_name: &str) -> bool {
429 let binding_suffixes = [
431 "Status",
432 "Intent",
433 "Use",
434 "Type",
435 "Mode",
436 "Code",
437 "Category",
438 "Priority",
439 "Severity",
440 "State",
441 "Kind",
442 "Outcome",
443 "Action",
444 "Meaning",
445 "Behavior",
446 "Operator",
447 "Required",
448 "Selection",
449 "Timing",
450 "Purpose",
451 "Scale",
452 "Modifier",
453 "Direction",
454 "Comparator",
455 "Relationship",
456 "Participation",
457 "Capability",
458 "Interaction",
459 "Support",
460 "Content",
461 "Hierarchy",
462 "Property",
463 "Aggregation",
464 "Slicing",
465 "Derivation",
466 "Representation",
467 "Handling",
468 "Version",
469 "Classification",
470 "Assurance",
471 "Calibration",
472 "Operational",
473 "Range",
474 "Significance",
475 "Request",
476 "Response",
477 "Preference",
478 "Strand",
479 "Channel",
480 "Trigger",
481 "Variable",
482 "Orientation",
483 "Repository",
484 "Element",
485 "Study",
486 "Subject",
487 "Result",
488 "Participant",
489 "Search",
490 "Filter",
491 "Xpath",
492 "Sort",
493 "System",
494 "Gender",
495 "Actuality",
496 "Criticality",
497 "Tolerance",
498 "Plan",
499 "Team",
500 "Item",
501 "Statement",
502 "Knowledge",
503 "Evaluation",
504 "Disposition",
505 "Invoice",
506 "Component",
507 "Issue",
508 "Header",
509 "Metric",
510 "Color",
511 "Network",
512 "Note",
513 "Observation",
514 "Parameter",
515 "Provenance",
516 "Entity",
517 "Role",
518 "Publication",
519 "Quality",
520 "Quantity",
521 "Artifact",
522 "Relation",
523 "Remittance",
524 "Report",
525 "Research",
526 "Resource",
527 "Restful",
528 "Sequence",
529 "Slot",
530 "Definition",
531 "Structure",
532 "Subscription",
533 "Substance",
534 "Supply",
535 "Delivery",
536 "Task",
537 "Udi",
538 "Units",
539 "Time",
540 "Verification",
541 "Versioning",
542 "Vision",
543 "Base",
544 "Eye",
545 "Confidentiality",
546 ];
547
548 let nested_exceptions = [
550 "BundleEntry",
551 "AccountGuarantor",
552 "AccountCoverage",
553 "ParametersParameter",
554 "MeasureReportGroup",
555 "EvidenceVariableCharacteristic",
556 "ImplementationGuideGlobal",
557 "RiskEvidenceSynthesisCertainty",
558 ];
559
560 if nested_exceptions.contains(&type_name) {
562 return false;
563 }
564
565 for suffix in &binding_suffixes {
567 if type_name.ends_with(suffix) {
568 if let Some(parent_resource) = Self::detect_nested_structure_parent(type_name) {
571 let expected_suffix_part = &type_name[parent_resource.len()..];
572 if binding_suffixes
575 .iter()
576 .any(|s| expected_suffix_part.ends_with(s))
577 {
578 return true;
579 }
580 }
581
582 if Self::detect_nested_structure_parent(type_name).is_none() {
586 return true;
587 }
588 }
589 }
590
591 false
592 }
593
594 fn get_trait_import_path(type_name: &str) -> String {
596 let module_name = if type_name.ends_with("Accessors") {
598 type_name.strip_suffix("Accessors").unwrap_or(type_name)
599 } else if type_name.ends_with("Mutators") {
600 type_name.strip_suffix("Mutators").unwrap_or(type_name)
601 } else if type_name.ends_with("Helpers") {
602 type_name.strip_suffix("Helpers").unwrap_or(type_name)
603 } else {
604 type_name
605 };
606
607 format!(
608 "crate::traits::{}::{}",
609 crate::naming::Naming::to_snake_case(module_name),
610 type_name
611 )
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn test_longest_prefix_matching() {
621 assert_eq!(
623 NamingManager::detect_nested_structure_parent("EvidenceVariableCharacteristic"),
624 Some("EvidenceVariable".to_string())
625 );
626 assert_eq!(
627 NamingManager::detect_nested_structure_parent("MeasureReportGroup"),
628 Some("MeasureReport".to_string())
629 );
630 assert_eq!(
631 NamingManager::detect_nested_structure_parent("AccountCoverage"),
632 Some("Account".to_string())
633 );
634 assert_eq!(
635 NamingManager::detect_nested_structure_parent("ImplementationGuideGlobal"),
636 Some("ImplementationGuide".to_string())
637 );
638
639 assert_eq!(
641 NamingManager::detect_nested_structure_parent("ConditionStage"),
642 Some("Condition".to_string())
643 );
644 }
645
646 #[test]
647 fn test_import_path_generation() {
648 assert_eq!(
650 NamingManager::get_import_path_for_type("EvidenceVariableCharacteristic"),
651 "crate::resources::evidence_variable::EvidenceVariableCharacteristic"
652 );
653 assert_eq!(
654 NamingManager::get_import_path_for_type("MeasureReportGroup"),
655 "crate::resources::measure_report::MeasureReportGroup"
656 );
657 assert_eq!(
658 NamingManager::get_import_path_for_type("AccountCoverage"),
659 "crate::resources::account::AccountCoverage"
660 );
661
662 assert_eq!(
664 NamingManager::get_import_path_for_type("Patient"),
665 "crate::resources::patient::Patient"
666 );
667 assert_eq!(
668 NamingManager::get_import_path_for_type("Identifier"),
669 "crate::datatypes::identifier::Identifier"
670 );
671
672 assert_eq!(
674 NamingManager::get_import_path_for_type("DomainResourceAccessors"),
675 "crate::traits::domain_resource::DomainResourceAccessors"
676 );
677 assert_eq!(
678 NamingManager::get_import_path_for_type("ResourceAccessors"),
679 "crate::traits::resource::ResourceAccessors"
680 );
681
682 assert_eq!(
684 NamingManager::get_import_path_for_type("RiskEvidenceSynthesisCertainty"),
685 "crate::resources::risk_evidence_synthesis::RiskEvidenceSynthesisCertainty"
686 );
687 assert_eq!(
688 NamingManager::get_import_path_for_type("ParametersParameter"),
689 "crate::resources::parameters::ParametersParameter"
690 );
691 }
692
693 #[test]
694 fn test_resource_and_datatype_detection() {
695 assert!(NamingManager::is_fhir_resource("Patient"));
696 assert!(NamingManager::is_fhir_resource("EvidenceVariable"));
697 assert!(NamingManager::is_fhir_resource("MeasureReport"));
698 assert!(!NamingManager::is_fhir_resource("Identifier"));
699
700 assert!(NamingManager::is_fhir_datatype("Identifier"));
701 assert!(NamingManager::is_fhir_datatype("CodeableConcept"));
702 assert!(!NamingManager::is_fhir_datatype("Patient"));
703 }
704
705 #[test]
706 fn test_edge_cases() {
707 assert_eq!(
709 NamingManager::detect_nested_structure_parent("Patient"),
710 None
711 );
712 assert_eq!(
713 NamingManager::detect_nested_structure_parent("Identifier"),
714 None
715 );
716
717 assert_eq!(
719 NamingManager::detect_nested_structure_parent("PatientSomething"),
720 Some("Patient".to_string())
721 );
722 }
723
724 #[test]
725 fn test_binding_enum_detection() {
726 assert!(NamingManager::is_likely_binding_enum("TaskIntent"));
728 assert!(NamingManager::is_likely_binding_enum("TaskStatus"));
729 assert!(NamingManager::is_likely_binding_enum("MedicationStatus"));
730 assert!(NamingManager::is_likely_binding_enum(
731 "MedicationAdminStatus"
732 ));
733 assert!(NamingManager::is_likely_binding_enum("PublicationStatus"));
734 assert!(NamingManager::is_likely_binding_enum("AddressUse"));
735 assert!(NamingManager::is_likely_binding_enum("ContactPointSystem"));
736 assert!(NamingManager::is_likely_binding_enum("RequestPriority"));
737
738 assert!(!NamingManager::is_likely_binding_enum("AccountCoverage"));
740 assert!(!NamingManager::is_likely_binding_enum("MeasureReportGroup"));
741 assert!(!NamingManager::is_likely_binding_enum(
742 "EvidenceVariableCharacteristic"
743 ));
744 assert!(!NamingManager::is_likely_binding_enum("BundleEntry"));
745
746 assert!(!NamingManager::is_likely_binding_enum("Patient"));
748 assert!(!NamingManager::is_likely_binding_enum("Identifier"));
749 assert!(!NamingManager::is_likely_binding_enum("CodeableConcept"));
750 }
751
752 #[test]
753 fn test_corrected_import_paths() {
754 assert_eq!(
756 NamingManager::get_import_path_for_type("TaskIntent"),
757 "crate::bindings::task_intent::TaskIntent"
758 );
759 assert_eq!(
760 NamingManager::get_import_path_for_type("TaskStatus"),
761 "crate::bindings::task_status::TaskStatus"
762 );
763 assert_eq!(
764 NamingManager::get_import_path_for_type("MedicationStatus"),
765 "crate::bindings::medication_status::MedicationStatus"
766 );
767 assert_eq!(
768 NamingManager::get_import_path_for_type("PublicationStatus"),
769 "crate::bindings::publication_status::PublicationStatus"
770 );
771
772 assert_eq!(
774 NamingManager::get_import_path_for_type("AccountCoverage"),
775 "crate::resources::account::AccountCoverage"
776 );
777 assert_eq!(
778 NamingManager::get_import_path_for_type("MeasureReportGroup"),
779 "crate::resources::measure_report::MeasureReportGroup"
780 );
781 assert_eq!(
783 NamingManager::get_import_path_for_type("ConditionStage"),
784 "crate::resources::condition::ConditionStage"
785 );
786 }
787}