1use crate::fhir_types::StructureDefinition;
67
68pub struct Naming;
70
71impl Naming {
72 pub fn struct_name(structure_def: &StructureDefinition) -> String {
78 let raw_name = if structure_def.name == "alternate" {
79 Self::to_rust_identifier(&structure_def.id)
81 } else if structure_def.name.is_empty() {
82 Self::to_rust_identifier(&structure_def.id)
84 } else if structure_def.name != structure_def.id && !structure_def.id.is_empty() {
85 Self::to_rust_identifier(&structure_def.id)
88 } else {
89 Self::to_rust_identifier(&structure_def.name)
91 };
92
93 if structure_def.kind != "primitive-type" {
95 Self::capitalize_first(&raw_name)
96 } else {
97 raw_name
98 }
99 }
100
101 pub fn field_name(name: &str) -> String {
107 let clean_name = if name.ends_with("[x]") {
109 name.strip_suffix("[x]").unwrap_or(name)
110 } else {
111 name
112 };
113
114 let conflict_resolved_name = if clean_name == "base" {
116 "base_definition"
118 } else {
119 clean_name
120 };
121
122 let snake_case = conflict_resolved_name
124 .chars()
125 .enumerate()
126 .map(|(i, c)| {
127 if c.is_uppercase() && i > 0 {
128 format!("_{}", c.to_lowercase())
129 } else {
130 c.to_lowercase().to_string()
131 }
132 })
133 .collect::<String>();
134
135 Self::handle_rust_keywords(&snake_case)
137 }
138
139 pub fn type_suffix(type_code: &str) -> String {
141 type_code
142 .chars()
143 .enumerate()
144 .map(|(i, c)| {
145 if c.is_uppercase() && i > 0 {
146 format!("_{}", c.to_lowercase())
147 } else {
148 c.to_lowercase().to_string()
149 }
150 })
151 .collect()
152 }
153
154 pub fn filename(structure_def: &StructureDefinition) -> String {
160 let struct_name = Self::struct_name(structure_def);
161 let snake_case_name = Self::to_snake_case(&struct_name);
162 format!("{snake_case_name}.rs")
163 }
164
165 pub fn enum_filename(enum_name: &str) -> String {
167 let snake_case_name = Self::to_snake_case(enum_name);
168 format!("{snake_case_name}.rs")
169 }
170
171 pub fn module_name(enum_name: &str) -> String {
177 Self::to_snake_case(enum_name)
178 }
179
180 pub fn trait_module_name(name: &str) -> String {
182 let cleaned = name
184 .replace([' ', '-', '.'], "_")
185 .replace(['(', ')', '[', ']'], "")
186 .replace(['/', '\\', ':'], "_");
187
188 Self::to_snake_case(&cleaned)
190 .chars()
191 .filter(|c| c.is_alphanumeric() || *c == '_')
192 .collect()
193 }
194
195 pub fn to_snake_case(name: &str) -> String {
201 let mut result = String::new();
202 let chars: Vec<char> = name.chars().collect();
203
204 for (i, &ch) in chars.iter().enumerate() {
205 if ch.is_uppercase() && i > 0 {
206 let is_acronym_continuation = i > 0 && chars[i - 1].is_uppercase();
208 let is_followed_by_lowercase = i + 1 < chars.len() && chars[i + 1].is_lowercase();
209
210 if (i > 0 && chars[i - 1].is_lowercase())
214 || (is_acronym_continuation && is_followed_by_lowercase)
215 {
216 result.push('_');
217 }
218 }
219
220 result.push(ch.to_lowercase().next().unwrap_or(ch));
221 }
222
223 result
224 }
225
226 pub fn to_pascal_case(s: &str) -> String {
228 s.split('_')
229 .map(|word| {
230 let mut chars = word.chars();
231 match chars.next() {
232 None => String::new(),
233 Some(first) => {
234 first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
235 }
236 }
237 })
238 .collect()
239 }
240
241 pub fn capitalize_first(s: &str) -> String {
243 if s.is_empty() {
244 return s.to_string();
245 }
246 s[0..1].to_uppercase() + &s[1..]
247 }
248
249 pub fn to_rust_identifier(name: &str) -> String {
255 if Self::is_valid_rust_identifier(name) {
257 return name.to_string();
258 }
259
260 let mut result = String::new();
262 let mut capitalize_next = true;
263
264 for ch in name.chars() {
265 if ch.is_alphanumeric() {
266 if capitalize_next {
267 result.push(ch.to_uppercase().next().unwrap_or(ch));
268 capitalize_next = false;
269 } else {
270 result.push(ch);
271 }
272 } else {
273 capitalize_next = true;
275 }
276 }
277
278 if result.is_empty() || result.chars().next().is_some_and(|c| c.is_numeric()) {
280 result = format!("_{result}");
281 }
282
283 Self::fix_acronyms(&result)
285 }
286
287 pub fn is_valid_rust_identifier(name: &str) -> bool {
289 if name.is_empty() {
290 return false;
291 }
292
293 let mut chars = name.chars();
294 let Some(first_char) = chars.next() else {
295 return false;
296 };
297
298 if !first_char.is_alphabetic() && first_char != '_' {
300 return false;
301 }
302
303 for ch in chars {
305 if !ch.is_alphanumeric() && ch != '_' {
306 return false;
307 }
308 }
309
310 !Self::is_rust_keyword(name)
312 }
313
314 pub fn is_rust_keyword(name: &str) -> bool {
316 matches!(
317 name,
318 "as" | "break"
319 | "const"
320 | "continue"
321 | "crate"
322 | "else"
323 | "enum"
324 | "extern"
325 | "false"
326 | "fn"
327 | "for"
328 | "if"
329 | "impl"
330 | "in"
331 | "let"
332 | "loop"
333 | "match"
334 | "mod"
335 | "move"
336 | "mut"
337 | "pub"
338 | "ref"
339 | "return"
340 | "self"
341 | "Self"
342 | "static"
343 | "struct"
344 | "super"
345 | "trait"
346 | "true"
347 | "type"
348 | "unsafe"
349 | "use"
350 | "where"
351 | "while"
352 | "async"
353 | "await"
354 | "dyn"
355 | "abstract"
356 | "become"
357 | "box"
358 | "do"
359 | "final"
360 | "macro"
361 | "override"
362 | "priv"
363 | "typeof"
364 | "unsized"
365 | "virtual"
366 | "yield"
367 | "try"
368 )
369 }
370
371 fn handle_rust_keywords(name: &str) -> String {
377 match name {
378 "type" => "type_".to_string(),
379 "match" => "match_".to_string(),
380 "loop" => "loop_".to_string(),
381 "move" => "move_".to_string(),
382 "ref" => "ref_".to_string(),
383 "mod" => "mod_".to_string(),
384 "use" => "use_".to_string(),
385 "self" => "self_".to_string(),
386 "super" => "super_".to_string(),
387 "crate" => "crate_".to_string(),
388 "async" => "async_".to_string(),
389 "await" => "await_".to_string(),
390 "fn" => "fn_".to_string(),
391 "let" => "let_".to_string(),
392 "const" => "const_".to_string(),
393 "static" => "static_".to_string(),
394 "struct" => "struct_".to_string(),
395 "enum" => "enum_".to_string(),
396 "impl" => "impl_".to_string(),
397 "trait" => "trait_".to_string(),
398 "for" => "for_".to_string(),
399 "if" => "if_".to_string(),
400 "else" => "else_".to_string(),
401 "while" => "while_".to_string(),
402 "return" => "return_".to_string(),
403 "where" => "where_".to_string(),
404 "abstract" => "abstract_".to_string(),
405 _ => name.to_string(),
406 }
407 }
408
409 fn fix_acronyms(name: &str) -> String {
411 let mut result = name.to_string();
412
413 let acronyms = [
415 ("Cqf", "CQF"), ("Fhir", "FHIR"), ("Hl7", "HL7"), ("Http", "HTTP"), ("Https", "HTTPS"), ("Json", "JSON"), ("Xml", "XML"), ("Uuid", "UUID"), ("Uri", "URI"), ("Url", "URL"), ("Api", "API"), ];
427
428 for (from, to) in &acronyms {
429 result = result.replace(from, to);
430 }
431
432 result
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn test_struct_name_generation() {
442 let structure = StructureDefinition {
443 resource_type: "StructureDefinition".to_string(),
444 id: "Patient".to_string(),
445 url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
446 name: "Patient".to_string(),
447 title: Some("Patient".to_string()),
448 status: "active".to_string(),
449 kind: "resource".to_string(),
450 is_abstract: false,
451 description: Some("A patient resource".to_string()),
452 purpose: None,
453 base_type: "DomainResource".to_string(),
454 base_definition: Some(
455 "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
456 ),
457 version: None,
458 differential: None,
459 snapshot: None,
460 };
461
462 assert_eq!(Naming::struct_name(&structure), "Patient");
463 }
464
465 #[test]
466 fn test_field_name_conversion() {
467 assert_eq!(Naming::field_name("active"), "active");
469 assert_eq!(Naming::field_name("name"), "name");
470
471 assert_eq!(Naming::field_name("birthDate"), "birth_date");
473 assert_eq!(
474 Naming::field_name("multipleBirthBoolean"),
475 "multiple_birth_boolean"
476 );
477
478 assert_eq!(Naming::field_name("value[x]"), "value");
480 assert_eq!(Naming::field_name("deceased[x]"), "deceased");
481
482 assert_eq!(Naming::field_name("type"), "type_");
484 assert_eq!(Naming::field_name("use"), "use_");
485 assert_eq!(Naming::field_name("ref"), "ref_");
486 assert_eq!(Naming::field_name("for"), "for_");
487 assert_eq!(Naming::field_name("match"), "match_");
488
489 assert_eq!(Naming::field_name("base"), "base_definition");
491 }
492
493 #[test]
494 fn test_filename_generation() {
495 let patient_structure = StructureDefinition {
496 resource_type: "StructureDefinition".to_string(),
497 id: "Patient".to_string(),
498 url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
499 name: "Patient".to_string(),
500 title: Some("Patient".to_string()),
501 status: "active".to_string(),
502 kind: "resource".to_string(),
503 is_abstract: false,
504 description: Some("A patient resource".to_string()),
505 purpose: None,
506 base_type: "DomainResource".to_string(),
507 base_definition: Some(
508 "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
509 ),
510 version: None,
511 differential: None,
512 snapshot: None,
513 };
514
515 assert_eq!(Naming::filename(&patient_structure), "patient.rs");
516
517 let structure_definition = StructureDefinition {
518 resource_type: "StructureDefinition".to_string(),
519 id: "StructureDefinition".to_string(),
520 url: "http://hl7.org/fhir/StructureDefinition/StructureDefinition".to_string(),
521 name: "StructureDefinition".to_string(),
522 title: Some("StructureDefinition".to_string()),
523 status: "active".to_string(),
524 kind: "resource".to_string(),
525 is_abstract: false,
526 description: Some("A structure definition resource".to_string()),
527 purpose: None,
528 base_type: "MetadataResource".to_string(),
529 base_definition: Some(
530 "http://hl7.org/fhir/StructureDefinition/MetadataResource".to_string(),
531 ),
532 version: None,
533 differential: None,
534 snapshot: None,
535 };
536
537 assert_eq!(
538 Naming::filename(&structure_definition),
539 "structure_definition.rs"
540 );
541 }
542
543 #[test]
544 fn test_snake_case_conversion() {
545 assert_eq!(Naming::to_snake_case("Patient"), "patient");
546 assert_eq!(
547 Naming::to_snake_case("StructureDefinition"),
548 "structure_definition"
549 );
550 assert_eq!(Naming::to_snake_case("HTTPRequest"), "http_request");
551 assert_eq!(Naming::to_snake_case("someField"), "some_field");
552 }
553
554 #[test]
555 fn test_pascal_case_conversion() {
556 assert_eq!(Naming::to_pascal_case("patient_name"), "PatientName");
557 assert_eq!(Naming::to_pascal_case("some_field"), "SomeField");
558 assert_eq!(Naming::to_pascal_case("http_request"), "HttpRequest");
559 }
560
561 #[test]
562 fn test_rust_identifier_conversion() {
563 assert_eq!(
565 Naming::to_rust_identifier("StructureDefinition"),
566 "StructureDefinition"
567 );
568 assert_eq!(Naming::to_rust_identifier("Patient"), "Patient");
569 assert_eq!(Naming::to_rust_identifier("Observation"), "Observation");
570 assert_eq!(Naming::to_rust_identifier("CodeSystem"), "CodeSystem");
571
572 assert_eq!(
574 Naming::to_rust_identifier("Relative Date Criteria"),
575 "RelativeDateCriteria"
576 );
577 assert_eq!(Naming::to_rust_identifier("Care Plan"), "CarePlan");
578
579 assert_eq!(Naming::to_rust_identifier("patient-name"), "PatientName");
581 assert_eq!(Naming::to_rust_identifier("patient_name"), "patient_name");
582
583 assert_eq!(
585 Naming::to_rust_identifier("some-complex_name with.spaces"),
586 "SomeComplexNameWithSpaces"
587 );
588
589 assert_eq!(Naming::to_rust_identifier(""), "_");
591 assert_eq!(Naming::to_rust_identifier(" "), "_");
592 assert_eq!(Naming::to_rust_identifier("a"), "a");
593 }
594
595 #[test]
596 fn test_type_suffix() {
597 assert_eq!(Naming::type_suffix("string"), "string");
598 assert_eq!(Naming::type_suffix("DateTime"), "date_time");
599 assert_eq!(Naming::type_suffix("CodeableConcept"), "codeable_concept");
600 }
601
602 #[test]
603 fn test_enum_filename() {
604 assert_eq!(Naming::enum_filename("PatientStatus"), "patient_status.rs");
605 assert_eq!(Naming::enum_filename("HTTPMethod"), "http_method.rs");
606 }
607
608 #[test]
609 fn test_trait_module_name() {
610 assert_eq!(Naming::trait_module_name("Patient"), "patient");
611 assert_eq!(
612 Naming::trait_module_name("Relative Date Criteria"),
613 "relative_date_criteria"
614 );
615 assert_eq!(Naming::trait_module_name("patient-name"), "patient_name");
616 }
617}