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());
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().unwrap().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 first_char = chars.next().unwrap();
151
152 if !first_char.is_alphabetic() && first_char != '_' {
154 return false;
155 }
156
157 for ch in chars {
159 if !ch.is_alphanumeric() && ch != '_' {
160 return false;
161 }
162 }
163
164 !Self::is_rust_keyword(name)
166 }
167
168 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 pub fn to_rust_field_name(name: &str) -> String {
227 let clean_name = if name.ends_with("[x]") {
229 name.strip_suffix("[x]").unwrap_or(name)
230 } else {
231 name
232 };
233
234 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 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 pub fn to_filename(structure_def: &StructureDefinition) -> String {
276 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 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 pub fn is_fhir_resource_type(type_name: &str) -> bool {
293 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 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 pub fn is_fhir_primitive_type(type_name: &str) -> bool {
693 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 pub fn is_generated_trait(type_name: &str) -> bool {
721 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 pub fn get_import_path_for_type(type_name: &str) -> String {
736 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 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 if Self::is_fhir_primitive_type(type_name) {
756 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 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 format!(
794 "crate::bindings::{}::{}",
795 Self::to_snake_case(type_name),
796 type_name
797 )
798 }
799}