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