Skip to main content

rh_codegen/generators/
utils.rs

1//! Utility functions for code generation
2//!
3//! This module contains utility functions used across different generators.
4
5use crate::fhir_types::StructureDefinition;
6
7/// Utility functions for code generation
8pub struct GeneratorUtils;
9
10impl GeneratorUtils {
11    /// Convert a string to PascalCase
12    pub fn to_pascal_case(s: &str) -> String {
13        s.split('_')
14            .map(|word| {
15                let mut chars = word.chars();
16                match chars.next() {
17                    Some(first) => {
18                        first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
19                    }
20                    None => String::new(),
21                }
22            })
23            .collect()
24    }
25
26    /// Convert a PascalCase type name to snake_case for module imports
27    pub fn to_snake_case(name: &str) -> String {
28        let mut result = String::new();
29        let chars: Vec<char> = name.chars().collect();
30
31        for (i, &ch) in chars.iter().enumerate() {
32            if ch.is_uppercase() && i > 0 {
33                // Check if this is part of an acronym or start of a new word
34                let is_acronym_continuation = i > 0 && chars[i - 1].is_uppercase();
35                let is_followed_by_lowercase = i + 1 < chars.len() && chars[i + 1].is_lowercase();
36
37                // Add underscore if:
38                // 1. Previous char was lowercase (start of new word like "someWord")
39                // 2. This is an acronym followed by lowercase (like "HTTPRequest" -> "http_request")
40                if (i > 0 && chars[i - 1].is_lowercase())
41                    || (is_acronym_continuation && is_followed_by_lowercase)
42                {
43                    result.push('_');
44                }
45            }
46
47            result.push(ch.to_lowercase().next().unwrap());
48        }
49
50        result
51    }
52
53    /// Capitalize the first letter of a string
54    pub fn capitalize_first_letter(s: &str) -> String {
55        s[0..1].to_uppercase() + &s[1..]
56    }
57
58    /// Generate a proper Rust struct name from StructureDefinition URL or ID
59    pub fn generate_struct_name(structure_def: &StructureDefinition) -> String {
60        let raw_name = if structure_def.name == "alternate" {
61            // Special case for "alternate" name - use ID
62            Self::to_valid_rust_identifier(&structure_def.id)
63        } else if structure_def.name.is_empty() {
64            // No name provided - use ID
65            Self::to_valid_rust_identifier(&structure_def.id)
66        } else if structure_def.name != structure_def.id && !structure_def.id.is_empty() {
67            // Name and ID differ - prefer ID for uniqueness, especially for extensions
68            // This handles cases like cqf-library where name="library" but id="cqf-library"
69            Self::to_valid_rust_identifier(&structure_def.id)
70        } else {
71            // Use name when it matches ID or ID is empty
72            Self::to_valid_rust_identifier(&structure_def.name)
73        };
74
75        // FHIR conventions is to have capitalized names for non-primitive types
76        if structure_def.kind != "primitive-type" {
77            Self::capitalize_first_letter(&raw_name)
78        } else {
79            raw_name
80        }
81    }
82
83    /// Convert a FHIR name to a valid Rust identifier while preserving the original as much as possible
84    pub fn to_valid_rust_identifier(name: &str) -> String {
85        // For names that are already valid Rust identifiers, use them as-is
86        if Self::is_valid_rust_identifier(name) {
87            return name.to_string();
88        }
89
90        // For names with spaces, dashes, or other characters, convert to PascalCase
91        let mut result = String::new();
92        let mut capitalize_next = true;
93
94        for ch in name.chars() {
95            if ch.is_alphanumeric() {
96                if capitalize_next {
97                    result.extend(ch.to_uppercase());
98                    capitalize_next = false;
99                } else {
100                    result.push(ch);
101                }
102            } else {
103                // Skip non-alphanumeric characters and capitalize the next letter
104                capitalize_next = true;
105            }
106        }
107
108        // Ensure it starts with a letter or underscore (Rust requirement)
109        if result.is_empty() || result.chars().next().unwrap().is_numeric() {
110            result = format!("_{result}");
111        }
112
113        // Handle common FHIR acronyms that should remain uppercase
114        Self::fix_acronyms(&result)
115    }
116
117    /// Fix common FHIR acronyms to maintain proper casing
118    pub fn fix_acronyms(name: &str) -> String {
119        let mut result = name.to_string();
120
121        // Common FHIR acronyms that should be uppercase
122        let acronyms = [
123            ("Cqf", "CQF"),     // Clinical Quality Framework
124            ("Fhir", "FHIR"),   // Fast Healthcare Interoperability Resources
125            ("Hl7", "HL7"),     // Health Level 7
126            ("Http", "HTTP"),   // HyperText Transfer Protocol
127            ("Https", "HTTPS"), // HTTP Secure
128            ("Json", "JSON"),   // JavaScript Object Notation
129            ("Xml", "XML"),     // eXtensible Markup Language
130            ("Uuid", "UUID"),   // Universally Unique Identifier
131            ("Uri", "URI"),     // Uniform Resource Identifier
132            ("Url", "URL"),     // Uniform Resource Locator
133            ("Api", "API"),     // Application Programming Interface
134        ];
135
136        for (from, to) in &acronyms {
137            result = result.replace(from, to);
138        }
139
140        result
141    }
142
143    /// Check if a string is a valid Rust identifier
144    pub fn is_valid_rust_identifier(name: &str) -> bool {
145        if name.is_empty() {
146            return false;
147        }
148
149        let mut chars = name.chars();
150        let first_char = chars.next().unwrap();
151
152        // First character must be a letter or underscore
153        if !first_char.is_alphabetic() && first_char != '_' {
154            return false;
155        }
156
157        // Remaining characters must be alphanumeric or underscore
158        for ch in chars {
159            if !ch.is_alphanumeric() && ch != '_' {
160                return false;
161            }
162        }
163
164        // Check if it's a Rust keyword
165        !Self::is_rust_keyword(name)
166    }
167
168    /// Check if a string is a Rust keyword
169    pub fn is_rust_keyword(name: &str) -> bool {
170        matches!(
171            name,
172            "as" | "break"
173                | "const"
174                | "continue"
175                | "crate"
176                | "else"
177                | "enum"
178                | "extern"
179                | "false"
180                | "fn"
181                | "for"
182                | "if"
183                | "impl"
184                | "in"
185                | "let"
186                | "loop"
187                | "match"
188                | "mod"
189                | "move"
190                | "mut"
191                | "pub"
192                | "ref"
193                | "return"
194                | "self"
195                | "Self"
196                | "static"
197                | "struct"
198                | "super"
199                | "trait"
200                | "true"
201                | "type"
202                | "unsafe"
203                | "use"
204                | "where"
205                | "while"
206                | "async"
207                | "await"
208                | "dyn"
209                | "abstract"
210                | "become"
211                | "box"
212                | "do"
213                | "final"
214                | "macro"
215                | "override"
216                | "priv"
217                | "typeof"
218                | "unsized"
219                | "virtual"
220                | "yield"
221                | "try"
222        )
223    }
224
225    /// Convert a FHIR field name to a valid Rust field name
226    pub fn to_rust_field_name(name: &str) -> String {
227        // Handle FHIR choice types (fields ending with [x])
228        let clean_name = if name.ends_with("[x]") {
229            name.strip_suffix("[x]").unwrap_or(name)
230        } else {
231            name
232        };
233
234        // Convert to snake_case and handle Rust keywords
235        let snake_case = clean_name
236            .chars()
237            .enumerate()
238            .map(|(i, c)| {
239                if c.is_uppercase() && i > 0 {
240                    format!("_{}", c.to_lowercase())
241                } else {
242                    c.to_lowercase().to_string()
243                }
244            })
245            .collect::<String>();
246
247        // Handle Rust keywords by appending underscore
248        match snake_case.as_str() {
249            "type" => "type_".to_string(),
250            "use" => "use_".to_string(),
251            "ref" => "ref_".to_string(),
252            "mod" => "mod_".to_string(),
253            "fn" => "fn_".to_string(),
254            "let" => "let_".to_string(),
255            "const" => "const_".to_string(),
256            "static" => "static_".to_string(),
257            "struct" => "struct_".to_string(),
258            "enum" => "enum_".to_string(),
259            "impl" => "impl_".to_string(),
260            "trait" => "trait_".to_string(),
261            "for" => "for_".to_string(),
262            "if" => "if_".to_string(),
263            "else" => "else_".to_string(),
264            "while" => "while_".to_string(),
265            "loop" => "loop_".to_string(),
266            "match" => "match_".to_string(),
267            "return" => "return_".to_string(),
268            "where" => "where_".to_string(),
269            "abstract" => "abstract_".to_string(),
270            _ => snake_case,
271        }
272    }
273
274    /// Convert a FHIR resource type name to filename using snake_case
275    pub fn to_filename(structure_def: &StructureDefinition) -> String {
276        // Use the struct name generation and convert to snake_case for filename
277        let struct_name = Self::generate_struct_name(structure_def);
278        let snake_case_name = Self::to_snake_case(&struct_name);
279
280        format!("{snake_case_name}.rs")
281    }
282
283    /// Check if a type name represents a primitive Rust type
284    pub fn is_primitive_type(type_name: &str) -> bool {
285        matches!(
286            type_name,
287            "String" | "i32" | "u32" | "i64" | "u64" | "f32" | "f64" | "bool" | "usize" | "isize"
288        )
289    }
290
291    /// Check if a type is a FHIR resource type
292    pub fn is_fhir_resource_type(type_name: &str) -> bool {
293        // Common FHIR resource types - using case-insensitive comparison
294        matches!(
295            type_name.to_lowercase().as_str(),
296            "account"
297                | "activitydefinition"
298                | "actordefinition"
299                | "administrableproductdefinition"
300                | "adverseevent"
301                | "allergyintolerance"
302                | "appointment"
303                | "appointmentresponse"
304                | "artifactassessment"
305                | "auditevent"
306                | "basic"
307                | "binary"
308                | "biologicallyderivedproduct"
309                | "biologicallyderivedproductdispense"
310                | "bodystructure"
311                | "bundle"
312                | "canonicalresource"
313                | "capabilitystatement"
314                | "careplan"
315                | "careteam"
316                | "chargeitem"
317                | "chargeitemdefinition"
318                | "citation"
319                | "claim"
320                | "claimresponse"
321                | "clinicalassessment"
322                | "clinicalusedefinition"
323                | "codesystem"
324                | "communication"
325                | "communicationrequest"
326                | "compartmentdefinition"
327                | "composition"
328                | "conceptmap"
329                | "condition"
330                | "conditiondefinition"
331                | "consent"
332                | "contract"
333                | "coverage"
334                | "coverageeligibilityrequest"
335                | "coverageeligibilityresponse"
336                | "detectedissue"
337                | "device"
338                | "devicealert"
339                | "deviceassociation"
340                | "devicedefinition"
341                | "devicedispense"
342                | "devicemetric"
343                | "devicerequest"
344                | "deviceusage"
345                | "diagnosticreport"
346                | "documentreference"
347                | "domainresource"
348                | "encounter"
349                | "encounterhistory"
350                | "endpoint"
351                | "enrollmentrequest"
352                | "enrollmentresponse"
353                | "episodeofcare"
354                | "eventdefinition"
355                | "evidence"
356                | "evidencevariable"
357                | "examplescenario"
358                | "explanationofbenefit"
359                | "familymemberhistory"
360                | "flag"
361                | "formularyitem"
362                | "genomicstudy"
363                | "goal"
364                | "graphdefinition"
365                | "group"
366                | "guidanceresponse"
367                | "healthcareservice"
368                | "imagingselection"
369                | "imagingstudy"
370                | "immunization"
371                | "immunizationevaluation"
372                | "immunizationrecommendation"
373                | "implementationguide"
374                | "ingredient"
375                | "insuranceplan"
376                | "insuranceproduct"
377                | "inventoryitem"
378                | "inventoryreport"
379                | "invoice"
380                | "library"
381                | "linkage"
382                | "list"
383                | "location"
384                | "manufactureditemdefinition"
385                | "measure"
386                | "measurereport"
387                | "medication"
388                | "medicationadministration"
389                | "medicationdispense"
390                | "medicationknowledge"
391                | "medicationrequest"
392                | "medicationstatement"
393                | "medicinalproductdefinition"
394                | "messagedefinition"
395                | "messageheader"
396                | "metadataresource"
397                | "moleculardefinition"
398                | "molecularsequence"
399                | "namingsystem"
400                | "nutritionintake"
401                | "nutritionorder"
402                | "nutritionproduct"
403                | "observation"
404                | "observationdefinition"
405                | "operationdefinition"
406                | "operationoutcome"
407                | "organization"
408                | "organizationaffiliation"
409                | "packagedproductdefinition"
410                | "parameters"
411                | "patient"
412                | "paymentnotice"
413                | "paymentreconciliation"
414                | "permission"
415                | "person"
416                | "personalrelationship"
417                | "plandefinition"
418                | "practitioner"
419                | "practitionerrole"
420                | "procedure"
421                | "provenance"
422                | "questionnaire"
423                | "questionnaireresponse"
424                | "regulatedauthorization"
425                | "relatedperson"
426                | "requestorchestration"
427                | "requirements"
428                | "researchstudy"
429                | "researchsubject"
430                | "resource"
431                | "riskassessment"
432                | "schedule"
433                | "searchparameter"
434                | "servicerequest"
435                | "slot"
436                | "specimen"
437                | "specimendefinition"
438                | "structuredefinition"
439                | "structuremap"
440                | "subscription"
441                | "subscriptionstatus"
442                | "subscriptiontopic"
443                | "substance"
444                | "substancedefinition"
445                | "substancenucleicacid"
446                | "substancepolymer"
447                | "substanceprotein"
448                | "substancereferenceinformation"
449                | "substancesourcematerial"
450                | "supplydelivery"
451                | "supplyrequest"
452                | "task"
453                | "terminologycapabilities"
454                | "testplan"
455                | "testreport"
456                | "testscript"
457                | "transport"
458                | "valueset"
459                | "verificationresult"
460                | "visionprescription"
461                | "bodysite"
462                | "catalogentry"
463                | "conformance"
464                | "dataelement"
465                | "devicecomponent"
466                | "deviceuserequest"
467                | "deviceusestatement"
468                | "diagnosticorder"
469                | "documentmanifest"
470                | "effectevidencesynthesis"
471                | "eligibilityrequest"
472                | "eligibilityresponse"
473                | "expansionprofile"
474                | "imagingmanifest"
475                | "imagingobjectselection"
476                | "media"
477                | "medicationorder"
478                | "medicationusage"
479                | "medicinalproduct"
480                | "medicinalproductauthorization"
481                | "medicinalproductcontraindication"
482                | "medicinalproductindication"
483                | "medicinalproductingredient"
484                | "medicinalproductinteraction"
485                | "medicinalproductmanufactured"
486                | "medicinalproductpackaged"
487                | "medicinalproductpharmaceutical"
488                | "medicinalproductundesirableeffect"
489                | "order"
490                | "orderresponse"
491                | "procedurerequest"
492                | "processrequest"
493                | "processresponse"
494                | "referralrequest"
495                | "requestgroup"
496                | "researchdefinition"
497                | "researchelementdefinition"
498                | "riskevidencesynthesis"
499                | "sequence"
500                | "servicedefinition"
501                | "substancespecification"
502        )
503    }
504
505    /// Check if a type name represents a known FHIR data type
506    pub fn is_fhir_datatype(name: &str) -> bool {
507        matches!(
508            name.to_lowercase().as_str(),
509            "base"
510                | "element"
511                | "backboneelement"
512                | "datatype"
513                | "address"
514                | "annotation"
515                | "attachment"
516                | "availability"
517                | "backbonetype"
518                | "dosage"
519                | "elementdefinition"
520                | "marketingstatus"
521                | "productshelflife"
522                | "relativetime"
523                | "timing"
524                | "codeableconcept"
525                | "codeablereference"
526                | "coding"
527                | "contactdetail"
528                | "contactpoint"
529                | "contributor"
530                | "datarequirement"
531                | "extendedcontactdetail"
532                | "humanname"
533                | "identifier"
534                | "monetarycomponent"
535                | "parameterdefinition"
536                | "period"
537                | "quantity"
538                | "range"
539                | "ratio"
540                | "ratiorange"
541                | "reference"
542                | "relatedartifact"
543                | "sampleddata"
544                | "signature"
545                | "triggerdefinition"
546                | "usagecontext"
547                | "virtualservicedetail"
548                | "base64binary"
549                | "boolean"
550                | "canonical"
551                | "code"
552                | "date"
553                | "datetime"
554                | "decimal"
555                | "id"
556                | "instant"
557                | "integer"
558                | "integer64"
559                | "markdown"
560                | "oid"
561                | "positiveint"
562                | "string"
563                | "time"
564                | "unsignedint"
565                | "uri"
566                | "url"
567                | "uuid"
568                | "xhtml"
569                | "accountguarantor"
570                | "accountdiagnosis"
571                | "activitydefinitionparticipant"
572                | "activitydefinitiondynamicvalue"
573                | "actordefinitioninput"
574                | "actordefinitionoutput"
575                | "adverseeventparticipant"
576                | "adverseeventsuspectentity"
577                | "adverseeventcontributingfactor"
578                | "adverseeventpreventiveaction"
579                | "adverseeventmitigatingaction"
580                | "adverseeventsupportinginfo"
581                | "allergyintolerancereaction"
582                | "appointmentparticipant"
583                | "appointmentrecurrencetemplate"
584                | "appointmentweeklytemplate"
585                | "appointmentmonthlytemplate"
586                | "appointmentyearlytemplate"
587                | "appointmentrecurrencetemplateweeklytemplate"
588                | "appointmentrecurrencetemplatemonthlytemplate"
589                | "appointmentrecurrencetemplateyearlytemplate"
590                | "artifactassessmentcontent"
591                | "artifactassessmentrelatedartifact"
592                | "auditeventagent"
593                | "auditeventsource"
594                | "auditevententity"
595                | "basicextension"
596                | "biologicallyderivedproductcollection"
597                | "biologicallyderivedproductprocessing"
598                | "biologicallyderivedproductmanipulation"
599                | "biologicallyderivedproductstorage"
600                | "biologicallyderivedproductdispenseperformer"
601                | "bodystructureincludedstructure"
602                | "bodystructureexcludedstructure"
603                | "bundleentry"
604                | "bundlelink"
605                | "bundlerequest"
606                | "bundleresponse"
607                | "capabilitystatementsoftware"
608                | "capabilitystatementimplementation"
609                | "capabilitystatementrest"
610                | "capabilitystatementmessaging"
611                | "careplanactivity"
612                | "careteamparticipant"
613                | "chargeitemperformer"
614                | "chargeitemdefinitionpropertygroup"
615                | "citationsummary"
616                | "citationabstract"
617                | "citationcitedartifact"
618                | "citationcontributorship"
619                | "claimitem"
620                | "claimsupportinginfo"
621                | "claimdiagnosis"
622                | "claimprocedure"
623                | "claiminsurance"
624                | "claimaccident"
625                | "claimpayee"
626                | "claimresponseitem"
627                | "claimresponseinsurance"
628                | "claimresponsepayment"
629                | "claimresponseprocessnote"
630                | "clinicalusedefinitionindication"
631                | "clinicalusedefinitioncontraindication"
632                | "clinicalusedefinitioninteraction"
633                | "clinicalusedefinitionundesirableeffect"
634                | "clinicalusedefinitionwarning"
635                | "codesystemconcept"
636                | "communicationpayload"
637                | "communicationrequestpayload"
638                | "compartmentdefinitionresource"
639                | "compositionattester"
640                | "compositionevent"
641                | "compositionsection"
642                | "conceptmapgroup"
643                | "conceptmapadditionalattribute"
644                | "conceptmaptarget"
645                | "conditionstage"
646                | "conditionevidence"
647                | "consentpolicy"
648                | "consentprovision"
649                | "contractterm"
650                | "contractfriendly"
651                | "contractlegal"
652                | "contractrule"
653                | "coverageclass"
654                | "coveragecosttobeneficiary"
655                | "coverageeligibilityrequestitem"
656                | "coverageeligibilityresponseitem"
657                | "detectedissuemitigation"
658                | "devicedevicename"
659                | "deviceproperty"
660                | "devicespecialization"
661                | "deviceversion"
662                | "devicedefinitionproperty"
663                | "devicemetriccalibration"
664                | "devicerequestparameter"
665                | "deviceusageadherence"
666                | "diagnosticreportmedia"
667                | "documentreferencerelatesto"
668                | "documentreferencecontent"
669                | "documentreferencecontext"
670                | "encounterparticipant"
671                | "encounterreason"
672                | "encounterdiagnosis"
673                | "encounterlocation"
674                | "endpointpayload"
675                | "episodeofcarediagnosis"
676                | "eventdefinitiontrigger"
677                | "evidencevariablecharacteristic"
678                | "examplescenarioactor"
679                | "examplescenarioinstance"
680                | "examplescenarioprocess"
681                | "explanationofbenefititem"
682                | "explanationofbenefitpayee"
683                | "explanationofbenefitdiagnosis"
684                | "explanationofbenefitprocedure"
685                | "explanationofbenefitinsurance"
686                | "explanationofbenefitaccident"
687                | "explanationofbenefitprocessnote"
688        )
689    }
690
691    /// Check if a type is a FHIR primitive type
692    pub fn is_fhir_primitive_type(type_name: &str) -> bool {
693        // FHIR primitive types that have extensions - matching actual generated type names
694        matches!(
695            type_name,
696            "StringType"
697                | "BooleanType"
698                | "IntegerType"
699                | "DecimalType"
700                | "UriType"
701                | "UrlType"
702                | "CanonicalType"
703                | "OidType"
704                | "UuidType"
705                | "InstantType"
706                | "DateType"
707                | "DateTimeType"
708                | "TimeType"
709                | "CodeType"
710                | "IdType"
711                | "MarkdownType"
712                | "Base64BinaryType"
713                | "UnsignedIntType"
714                | "PositiveIntType"
715                | "XhtmlType"
716        )
717    }
718
719    /// Check if a type is a generated trait
720    pub fn is_generated_trait(type_name: &str) -> bool {
721        // Traits are typically generated for base types or common interfaces
722        let lower_name = type_name.to_lowercase();
723        lower_name.ends_with("trait")
724            || matches!(
725                lower_name.as_str(),
726                "resourcetrait"
727                    | "domainresourcetrait"
728                    | "backboneelementtrait"
729                    | "elementtrait"
730                    | "metadataresourcetrait"
731            )
732    }
733
734    /// Determine the correct import path for a given type name
735    pub fn get_import_path_for_type(type_name: &str) -> String {
736        // Check if it's a known FHIR resource type
737        if Self::is_fhir_resource_type(type_name) {
738            return format!(
739                "crate::resources::{}::{}",
740                Self::to_snake_case(type_name),
741                type_name
742            );
743        }
744
745        // Check if it's a known FHIR datatype (use existing method for consistency)
746        if Self::is_fhir_datatype(type_name) {
747            return format!(
748                "crate::datatypes::{}::{}",
749                Self::to_snake_case(type_name),
750                type_name
751            );
752        }
753
754        // Check if it's a known primitive type extension
755        if Self::is_fhir_primitive_type(type_name) {
756            // Map type names to correct module names
757            let module_name = match type_name {
758                "StringType" => "string",
759                "BooleanType" => "boolean",
760                "IntegerType" => "integer",
761                "DecimalType" => "decimal",
762                "UriType" => "uri",
763                "UrlType" => "url",
764                "CanonicalType" => "canonical",
765                "OidType" => "oid",
766                "UuidType" => "uuid",
767                "InstantType" => "instant",
768                "DateType" => "date",
769                "DateTimeType" => "date_time",
770                "TimeType" => "time",
771                "CodeType" => "code",
772                "IdType" => "id",
773                "MarkdownType" => "markdown",
774                "Base64BinaryType" => "base64binary",
775                "UnsignedIntType" => "unsigned_int",
776                "PositiveIntType" => "positive_int",
777                "XhtmlType" => "xhtml",
778                _ => "unknown_primitive",
779            };
780            return format!("crate::primitives::{module_name}::{type_name}");
781        }
782
783        // Check if it's a generated trait
784        if Self::is_generated_trait(type_name) {
785            return format!(
786                "crate::traits::{}::{}",
787                Self::to_snake_case(type_name),
788                type_name
789            );
790        }
791
792        // Default to bindings for unknown types (likely enums)
793        format!(
794            "crate::bindings::{}::{}",
795            Self::to_snake_case(type_name),
796            type_name
797        )
798    }
799}