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());
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());
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().unwrap().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 first_char = chars.next().unwrap();
295
296 if !first_char.is_alphabetic() && first_char != '_' {
298 return false;
299 }
300
301 for ch in chars {
303 if !ch.is_alphanumeric() && ch != '_' {
304 return false;
305 }
306 }
307
308 !Self::is_rust_keyword(name)
310 }
311
312 pub fn is_rust_keyword(name: &str) -> bool {
314 matches!(
315 name,
316 "as" | "break"
317 | "const"
318 | "continue"
319 | "crate"
320 | "else"
321 | "enum"
322 | "extern"
323 | "false"
324 | "fn"
325 | "for"
326 | "if"
327 | "impl"
328 | "in"
329 | "let"
330 | "loop"
331 | "match"
332 | "mod"
333 | "move"
334 | "mut"
335 | "pub"
336 | "ref"
337 | "return"
338 | "self"
339 | "Self"
340 | "static"
341 | "struct"
342 | "super"
343 | "trait"
344 | "true"
345 | "type"
346 | "unsafe"
347 | "use"
348 | "where"
349 | "while"
350 | "async"
351 | "await"
352 | "dyn"
353 | "abstract"
354 | "become"
355 | "box"
356 | "do"
357 | "final"
358 | "macro"
359 | "override"
360 | "priv"
361 | "typeof"
362 | "unsized"
363 | "virtual"
364 | "yield"
365 | "try"
366 )
367 }
368
369 fn handle_rust_keywords(name: &str) -> String {
375 match name {
376 "type" => "type_".to_string(),
377 "match" => "match_".to_string(),
378 "loop" => "loop_".to_string(),
379 "move" => "move_".to_string(),
380 "ref" => "ref_".to_string(),
381 "mod" => "mod_".to_string(),
382 "use" => "use_".to_string(),
383 "self" => "self_".to_string(),
384 "super" => "super_".to_string(),
385 "crate" => "crate_".to_string(),
386 "async" => "async_".to_string(),
387 "await" => "await_".to_string(),
388 "fn" => "fn_".to_string(),
389 "let" => "let_".to_string(),
390 "const" => "const_".to_string(),
391 "static" => "static_".to_string(),
392 "struct" => "struct_".to_string(),
393 "enum" => "enum_".to_string(),
394 "impl" => "impl_".to_string(),
395 "trait" => "trait_".to_string(),
396 "for" => "for_".to_string(),
397 "if" => "if_".to_string(),
398 "else" => "else_".to_string(),
399 "while" => "while_".to_string(),
400 "return" => "return_".to_string(),
401 "where" => "where_".to_string(),
402 "abstract" => "abstract_".to_string(),
403 _ => name.to_string(),
404 }
405 }
406
407 fn fix_acronyms(name: &str) -> String {
409 let mut result = name.to_string();
410
411 let acronyms = [
413 ("Cqf", "CQF"), ("Fhir", "FHIR"), ("Hl7", "HL7"), ("Http", "HTTP"), ("Https", "HTTPS"), ("Json", "JSON"), ("Xml", "XML"), ("Uuid", "UUID"), ("Uri", "URI"), ("Url", "URL"), ("Api", "API"), ];
425
426 for (from, to) in &acronyms {
427 result = result.replace(from, to);
428 }
429
430 result
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_struct_name_generation() {
440 let structure = StructureDefinition {
441 resource_type: "StructureDefinition".to_string(),
442 id: "Patient".to_string(),
443 url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
444 name: "Patient".to_string(),
445 title: Some("Patient".to_string()),
446 status: "active".to_string(),
447 kind: "resource".to_string(),
448 is_abstract: false,
449 description: Some("A patient resource".to_string()),
450 purpose: None,
451 base_type: "DomainResource".to_string(),
452 base_definition: Some(
453 "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
454 ),
455 version: None,
456 differential: None,
457 snapshot: None,
458 };
459
460 assert_eq!(Naming::struct_name(&structure), "Patient");
461 }
462
463 #[test]
464 fn test_field_name_conversion() {
465 assert_eq!(Naming::field_name("active"), "active");
467 assert_eq!(Naming::field_name("name"), "name");
468
469 assert_eq!(Naming::field_name("birthDate"), "birth_date");
471 assert_eq!(
472 Naming::field_name("multipleBirthBoolean"),
473 "multiple_birth_boolean"
474 );
475
476 assert_eq!(Naming::field_name("value[x]"), "value");
478 assert_eq!(Naming::field_name("deceased[x]"), "deceased");
479
480 assert_eq!(Naming::field_name("type"), "type_");
482 assert_eq!(Naming::field_name("use"), "use_");
483 assert_eq!(Naming::field_name("ref"), "ref_");
484 assert_eq!(Naming::field_name("for"), "for_");
485 assert_eq!(Naming::field_name("match"), "match_");
486
487 assert_eq!(Naming::field_name("base"), "base_definition");
489 }
490
491 #[test]
492 fn test_filename_generation() {
493 let patient_structure = StructureDefinition {
494 resource_type: "StructureDefinition".to_string(),
495 id: "Patient".to_string(),
496 url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
497 name: "Patient".to_string(),
498 title: Some("Patient".to_string()),
499 status: "active".to_string(),
500 kind: "resource".to_string(),
501 is_abstract: false,
502 description: Some("A patient resource".to_string()),
503 purpose: None,
504 base_type: "DomainResource".to_string(),
505 base_definition: Some(
506 "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
507 ),
508 version: None,
509 differential: None,
510 snapshot: None,
511 };
512
513 assert_eq!(Naming::filename(&patient_structure), "patient.rs");
514
515 let structure_definition = StructureDefinition {
516 resource_type: "StructureDefinition".to_string(),
517 id: "StructureDefinition".to_string(),
518 url: "http://hl7.org/fhir/StructureDefinition/StructureDefinition".to_string(),
519 name: "StructureDefinition".to_string(),
520 title: Some("StructureDefinition".to_string()),
521 status: "active".to_string(),
522 kind: "resource".to_string(),
523 is_abstract: false,
524 description: Some("A structure definition resource".to_string()),
525 purpose: None,
526 base_type: "MetadataResource".to_string(),
527 base_definition: Some(
528 "http://hl7.org/fhir/StructureDefinition/MetadataResource".to_string(),
529 ),
530 version: None,
531 differential: None,
532 snapshot: None,
533 };
534
535 assert_eq!(
536 Naming::filename(&structure_definition),
537 "structure_definition.rs"
538 );
539 }
540
541 #[test]
542 fn test_snake_case_conversion() {
543 assert_eq!(Naming::to_snake_case("Patient"), "patient");
544 assert_eq!(
545 Naming::to_snake_case("StructureDefinition"),
546 "structure_definition"
547 );
548 assert_eq!(Naming::to_snake_case("HTTPRequest"), "http_request");
549 assert_eq!(Naming::to_snake_case("someField"), "some_field");
550 }
551
552 #[test]
553 fn test_pascal_case_conversion() {
554 assert_eq!(Naming::to_pascal_case("patient_name"), "PatientName");
555 assert_eq!(Naming::to_pascal_case("some_field"), "SomeField");
556 assert_eq!(Naming::to_pascal_case("http_request"), "HttpRequest");
557 }
558
559 #[test]
560 fn test_rust_identifier_conversion() {
561 assert_eq!(
563 Naming::to_rust_identifier("StructureDefinition"),
564 "StructureDefinition"
565 );
566 assert_eq!(Naming::to_rust_identifier("Patient"), "Patient");
567 assert_eq!(Naming::to_rust_identifier("Observation"), "Observation");
568 assert_eq!(Naming::to_rust_identifier("CodeSystem"), "CodeSystem");
569
570 assert_eq!(
572 Naming::to_rust_identifier("Relative Date Criteria"),
573 "RelativeDateCriteria"
574 );
575 assert_eq!(Naming::to_rust_identifier("Care Plan"), "CarePlan");
576
577 assert_eq!(Naming::to_rust_identifier("patient-name"), "PatientName");
579 assert_eq!(Naming::to_rust_identifier("patient_name"), "patient_name");
580
581 assert_eq!(
583 Naming::to_rust_identifier("some-complex_name with.spaces"),
584 "SomeComplexNameWithSpaces"
585 );
586
587 assert_eq!(Naming::to_rust_identifier(""), "_");
589 assert_eq!(Naming::to_rust_identifier(" "), "_");
590 assert_eq!(Naming::to_rust_identifier("a"), "a");
591 }
592
593 #[test]
594 fn test_type_suffix() {
595 assert_eq!(Naming::type_suffix("string"), "string");
596 assert_eq!(Naming::type_suffix("DateTime"), "date_time");
597 assert_eq!(Naming::type_suffix("CodeableConcept"), "codeable_concept");
598 }
599
600 #[test]
601 fn test_enum_filename() {
602 assert_eq!(Naming::enum_filename("PatientStatus"), "patient_status.rs");
603 assert_eq!(Naming::enum_filename("HTTPMethod"), "http_method.rs");
604 }
605
606 #[test]
607 fn test_trait_module_name() {
608 assert_eq!(Naming::trait_module_name("Patient"), "patient");
609 assert_eq!(
610 Naming::trait_module_name("Relative Date Criteria"),
611 "relative_date_criteria"
612 );
613 assert_eq!(Naming::trait_module_name("patient-name"), "patient_name");
614 }
615}