rh_codegen/generators/
utils.rs1use crate::fhir_types::StructureDefinition;
6
7pub struct GeneratorUtils;
9
10impl GeneratorUtils {
11 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 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 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 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 pub fn capitalize_first_letter(s: &str) -> String {
55 s[0..1].to_uppercase() + &s[1..]
56 }
57
58 pub fn generate_struct_name(structure_def: &StructureDefinition) -> String {
60 let raw_name = if structure_def.name == "alternate" {
61 Self::to_valid_rust_identifier(&structure_def.id)
63 } else if structure_def.name.is_empty() {
64 Self::to_valid_rust_identifier(&structure_def.id)
66 } else if structure_def.name != structure_def.id && !structure_def.id.is_empty() {
67 Self::to_valid_rust_identifier(&structure_def.id)
70 } else {
71 Self::to_valid_rust_identifier(&structure_def.name)
73 };
74
75 if structure_def.kind != "primitive-type" {
77 Self::capitalize_first_letter(&raw_name)
78 } else {
79 raw_name
80 }
81 }
82
83 pub fn to_valid_rust_identifier(name: &str) -> String {
85 if Self::is_valid_rust_identifier(name) {
87 return name.to_string();
88 }
89
90 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 capitalize_next = true;
105 }
106 }
107
108 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_numeric()) {
110 result = format!("_{result}");
111 }
112
113 Self::fix_acronyms(&result)
115 }
116
117 pub fn fix_acronyms(name: &str) -> String {
119 let mut result = name.to_string();
120
121 let acronyms = [
123 ("Cqf", "CQF"), ("Fhir", "FHIR"), ("Hl7", "HL7"), ("Http", "HTTP"), ("Https", "HTTPS"), ("Json", "JSON"), ("Xml", "XML"), ("Uuid", "UUID"), ("Uri", "URI"), ("Url", "URL"), ("Api", "API"), ];
135
136 for (from, to) in &acronyms {
137 result = result.replace(from, to);
138 }
139
140 result
141 }
142
143 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 if !first_char.is_alphabetic() && first_char != '_' {
156 return false;
157 }
158
159 for ch in chars {
161 if !ch.is_alphanumeric() && ch != '_' {
162 return false;
163 }
164 }
165
166 !Self::is_rust_keyword(name)
168 }
169
170 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 pub fn to_rust_field_name(name: &str) -> String {
229 let clean_name = if name.ends_with("[x]") {
231 name.strip_suffix("[x]").unwrap_or(name)
232 } else {
233 name
234 };
235
236 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 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 pub fn to_filename(structure_def: &StructureDefinition) -> String {
278 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 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 pub fn is_fhir_resource_type(type_name: &str) -> bool {
295 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 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 pub fn is_fhir_primitive_type(type_name: &str) -> bool {
695 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 pub fn is_generated_trait(type_name: &str) -> bool {
723 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 pub fn get_import_path_for_type(type_name: &str) -> String {
738 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 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 if Self::is_fhir_primitive_type(type_name) {
758 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 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 format!(
796 "crate::bindings::{}::{}",
797 Self::to_snake_case(type_name),
798 type_name
799 )
800 }
801}