1use crate::fhir_types::{ElementDefinition, StructureDefinition};
10use crate::metadata::{
11 FhirFieldType, FhirPrimitiveType, FieldInfo, MetadataRegistry, TypeMetadata,
12};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum MetadataCategory {
18 Resources,
19 Datatypes,
20 Primitives,
21 Profiles,
22 Other,
23}
24
25impl MetadataCategory {
26 fn module_name(self) -> &'static str {
27 match self {
28 Self::Resources => "resources",
29 Self::Datatypes => "datatypes",
30 Self::Primitives => "primitives",
31 Self::Profiles => "profiles",
32 Self::Other => "other",
33 }
34 }
35
36 fn doc_label(self) -> &'static str {
37 match self {
38 Self::Resources => "FHIR resources",
39 Self::Datatypes => "FHIR datatypes",
40 Self::Primitives => "FHIR primitive types",
41 Self::Profiles => "FHIR profiles",
42 Self::Other => "other FHIR types",
43 }
44 }
45}
46
47fn classify_metadata(sd: &StructureDefinition) -> MetadataCategory {
49 match sd.kind.as_str() {
50 "resource" => MetadataCategory::Resources,
51 "complex-type" => MetadataCategory::Datatypes,
52 "primitive-type" => MetadataCategory::Primitives,
53 _ => MetadataCategory::Other,
54 }
55}
56
57fn metadata_category_priority(category: MetadataCategory) -> u8 {
58 match category {
59 MetadataCategory::Primitives => 0,
60 MetadataCategory::Datatypes => 1,
61 MetadataCategory::Resources => 2,
62 MetadataCategory::Profiles => 3,
63 MetadataCategory::Other => 4,
64 }
65}
66
67fn sorted_structure_definitions(
68 structure_defs: &[StructureDefinition],
69) -> Vec<&StructureDefinition> {
70 let mut sorted_defs: Vec<_> = structure_defs.iter().collect();
71 sorted_defs.sort_by(|left, right| {
72 left.name
73 .cmp(&right.name)
74 .then_with(|| {
75 metadata_category_priority(classify_metadata(left))
76 .cmp(&metadata_category_priority(classify_metadata(right)))
77 })
78 .then_with(|| left.url.cmp(&right.url))
79 .then_with(|| left.id.cmp(&right.id))
80 });
81 sorted_defs
82}
83
84pub fn build_metadata_registry(structure_defs: &[StructureDefinition]) -> MetadataRegistry {
86 let mut registry = MetadataRegistry::new();
87
88 for structure_def in sorted_structure_definitions(structure_defs) {
89 if let Some(type_metadata) = extract_type_metadata(structure_def) {
90 if !registry.types.contains_key(&type_metadata.name) {
92 registry.add_type(type_metadata);
93 }
94 }
95 }
96
97 registry
98}
99
100fn extract_type_metadata(structure_def: &StructureDefinition) -> Option<TypeMetadata> {
102 let type_name = structure_def.name.as_str();
103 let mut fields = HashMap::new();
104
105 let snapshot = structure_def.snapshot.as_ref()?;
107 let elements = &snapshot.element;
108
109 for element in elements.iter().skip(1) {
111 if let Some(field_info) = extract_field_info(element, type_name) {
112 if let Some(field_name) = extract_field_name(&element.path, type_name) {
113 fields.insert(field_name, field_info);
114 }
115 }
116 }
117
118 Some(TypeMetadata {
119 name: type_name.to_string(),
120 fields,
121 })
122}
123
124fn extract_field_name(path: &str, type_name: &str) -> Option<String> {
126 let prefix = format!("{type_name}.");
127
128 if !path.starts_with(&prefix) {
129 return None;
130 }
131
132 let field_path = &path[prefix.len()..];
133
134 let field_name = field_path.split('.').next()?;
137
138 Some(field_name.to_string())
139}
140
141fn extract_field_info(element: &ElementDefinition, _type_name: &str) -> Option<FieldInfo> {
143 let element_types = element.element_type.as_ref()?;
144
145 if element_types.is_empty() {
146 return None;
147 }
148
149 let min = element.min.unwrap_or(0);
151 let max = element.max.as_ref().and_then(|m| {
152 if m == "*" {
153 None
154 } else {
155 m.parse::<u32>().ok()
156 }
157 });
158
159 let is_choice_type = element.path.contains("[x]");
161
162 let choice_types: Vec<String> = element_types
164 .iter()
165 .filter_map(|et| et.code.clone())
166 .collect();
167
168 let primary_type_code = element_types[0].code.as_ref()?;
170 let field_type = determine_field_type(primary_type_code);
171
172 Some(FieldInfo {
173 field_type,
174 min,
175 max,
176 is_choice_type,
177 choice_types,
178 })
179}
180
181fn determine_field_type(type_code: &str) -> FhirFieldType {
183 if let Some(primitive) = FhirPrimitiveType::from_fhir_type(type_code) {
185 return FhirFieldType::Primitive(primitive);
186 }
187
188 if type_code == "Reference" {
190 return FhirFieldType::Reference;
191 }
192
193 if type_code == "BackboneElement" {
195 return FhirFieldType::BackboneElement(type_code.to_string());
196 }
197
198 FhirFieldType::Complex(type_code.to_string())
200}
201
202pub fn generate_metadata_code(registry: &MetadataRegistry) -> String {
204 let mut code = String::new();
205
206 code.push_str(
208 r#"//! FHIR type metadata
209//!
210//! This module provides compile-time metadata about FHIR types, enabling
211//! path resolution like "Patient.name.given" -> FhirPrimitiveType::String.
212//!
213//! Generated automatically - do not edit manually.
214
215use phf::{phf_map, Map};
216
217"#,
218 );
219
220 code.push_str(
222 r#"/// FHIR primitive types
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
224pub enum FhirPrimitiveType {
225 Boolean,
226 Integer,
227 String,
228 Date,
229 DateTime,
230 Instant,
231 Time,
232 Decimal,
233 Uri,
234 Url,
235 Canonical,
236 Code,
237 Oid,
238 Id,
239 Markdown,
240 Base64Binary,
241 UnsignedInt,
242 PositiveInt,
243}
244
245"#,
246 );
247
248 code.push_str(
250 r#"/// FHIR field type (primitive, complex, reference, or backbone element)
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub enum FhirFieldType {
253 Primitive(FhirPrimitiveType),
254 Complex(&'static str),
255 Reference,
256 BackboneElement(&'static str),
257}
258
259"#,
260 );
261
262 code.push_str(
264 r#"/// Information about a field in a FHIR resource or datatype
265#[derive(Debug, Clone)]
266pub struct FieldInfo {
267 pub field_type: FhirFieldType,
268 pub min: u32,
269 pub max: Option<u32>,
270 pub is_choice_type: bool,
271}
272
273"#,
274 );
275
276 let mut generated_consts = std::collections::HashSet::new();
278
279 let mut sorted_types: Vec<_> = registry.types.iter().collect();
280 sorted_types.sort_by_key(|(left_name, _)| *left_name);
281
282 for (type_name, type_metadata) in sorted_types {
284 let sanitized_name: String = type_name
286 .chars()
287 .map(|c| if c.is_alphanumeric() { c } else { '_' })
288 .collect();
289 let const_name = format!("{}_FIELDS", sanitized_name.to_uppercase());
290
291 if generated_consts.contains(&const_name) {
293 continue;
294 }
295
296 generate_type_map(&mut code, type_name, type_metadata);
297 generated_consts.insert(const_name);
298 }
299
300 generate_registry_map(&mut code, ®istry.types, &generated_consts);
302
303 code.push_str(
305 r#"
306/// Get field information for a specific field in a type
307pub fn get_field_info(type_name: &str, field_name: &str) -> Option<&'static FieldInfo> {
308 FHIR_TYPE_REGISTRY
309 .get(type_name)
310 .and_then(|fields| fields.get(field_name))
311}
312
313/// Resolve a nested path like "Patient.name.given" to its field type
314pub fn resolve_path(path: &str) -> Option<&'static FhirFieldType> {
315 let parts: Vec<&str> = path.split('.').collect();
316 if parts.is_empty() {
317 return None;
318 }
319
320 let mut current_type_name = parts[0];
321
322 for (idx, &field_name) in parts[1..].iter().enumerate() {
323 let field_info = get_field_info(current_type_name, field_name)?;
324
325 // If this is the last field, return its type
326 if idx == parts.len() - 2 {
327 return Some(&field_info.field_type);
328 }
329
330 // Otherwise, navigate to the next type
331 match &field_info.field_type {
332 FhirFieldType::Complex(type_name) | FhirFieldType::BackboneElement(type_name) => {
333 current_type_name = type_name;
334 }
335 _ => return None, // Can't navigate further
336 }
337 }
338
339 None
340}
341"#,
342 );
343
344 code
345}
346
347fn generate_type_map(code: &mut String, type_name: &str, type_metadata: &TypeMetadata) {
349 let sanitized_name: String = type_name
352 .chars()
353 .map(|c| if c.is_alphanumeric() { c } else { '_' })
354 .collect();
355 let const_name = format!("{}_FIELDS", sanitized_name.to_uppercase());
356
357 code.push_str(&format!("/// Field metadata for {type_name}\n"));
358 code.push_str("#[rustfmt::skip]\n");
359 code.push_str(&format!(
360 "pub static {const_name}: Map<&'static str, FieldInfo> = phf_map! {{\n"
361 ));
362
363 let mut sorted_fields: Vec<_> = type_metadata.fields.iter().collect();
364 sorted_fields.sort_by_key(|(left_name, _)| *left_name);
365
366 for (field_name, field_info) in sorted_fields {
367 code.push_str(&format!(" \"{field_name}\" => FieldInfo {{\n"));
368
369 code.push_str(" field_type: ");
371 match &field_info.field_type {
372 FhirFieldType::Primitive(prim) => {
373 code.push_str(&format!(
374 "FhirFieldType::Primitive(FhirPrimitiveType::{})",
375 prim.variant_name()
376 ));
377 }
378 FhirFieldType::Complex(name) => {
379 code.push_str(&format!("FhirFieldType::Complex(\"{name}\")"));
380 }
381 FhirFieldType::Reference => {
382 code.push_str("FhirFieldType::Reference");
383 }
384 FhirFieldType::BackboneElement(name) => {
385 code.push_str(&format!("FhirFieldType::BackboneElement(\"{name}\")"));
386 }
387 }
388 code.push_str(",\n");
389
390 code.push_str(&format!(" min: {},\n", field_info.min));
392 code.push_str(" max: ");
393 if let Some(max) = field_info.max {
394 code.push_str(&format!("Some({max})"));
395 } else {
396 code.push_str("None");
397 }
398 code.push_str(",\n");
399
400 code.push_str(&format!(
402 " is_choice_type: {},\n",
403 field_info.is_choice_type
404 ));
405
406 code.push_str(" },\n");
407 }
408
409 code.push_str("};\n\n");
410}
411
412fn generate_registry_map(
414 code: &mut String,
415 types: &HashMap<String, TypeMetadata>,
416 generated_consts: &std::collections::HashSet<String>,
417) {
418 code.push_str("/// Main FHIR type registry mapping type names to their field metadata\n");
419 code.push_str(
420 "pub static FHIR_TYPE_REGISTRY: Map<&'static str, &'static Map<&'static str, FieldInfo>> = phf_map! {\n",
421 );
422
423 let mut sorted_type_names: Vec<_> = types.keys().collect();
424 sorted_type_names.sort();
425
426 for type_name in sorted_type_names {
427 let sanitized_name: String = type_name
428 .chars()
429 .map(|c| if c.is_alphanumeric() { c } else { '_' })
430 .collect();
431 let const_name = format!("{}_FIELDS", sanitized_name.to_uppercase());
432
433 if generated_consts.contains(&const_name) {
434 code.push_str(&format!(" \"{type_name}\" => &{const_name},\n"));
435 }
436 }
437
438 code.push_str("};\n");
439}
440
441pub fn generate_metadata_code_split(
446 registry: &MetadataRegistry,
447 structure_defs: &[StructureDefinition],
448) -> HashMap<String, String> {
449 let mut files = HashMap::new();
450
451 let mut name_to_category: HashMap<&str, MetadataCategory> = HashMap::new();
453 for sd in sorted_structure_definitions(structure_defs) {
454 name_to_category
455 .entry(&sd.name)
456 .or_insert_with(|| classify_metadata(sd));
457 }
458
459 let mut categories: HashMap<MetadataCategory, Vec<(&String, &TypeMetadata)>> = HashMap::new();
461 let mut sorted_registry_types: Vec<_> = registry.types.iter().collect();
462 sorted_registry_types.sort_by_key(|(left_name, _)| *left_name);
463
464 for (type_name, type_metadata) in sorted_registry_types {
465 let category = name_to_category
466 .get(type_name.as_str())
467 .copied()
468 .unwrap_or(MetadataCategory::Other);
469 categories
470 .entry(category)
471 .or_default()
472 .push((type_name, type_metadata));
473 }
474
475 let mut all_generated_consts = std::collections::HashSet::new();
477
478 let category_order = [
480 MetadataCategory::Resources,
481 MetadataCategory::Datatypes,
482 MetadataCategory::Primitives,
483 MetadataCategory::Profiles,
484 MetadataCategory::Other,
485 ];
486
487 for &category in &category_order {
488 let mut types = categories.get(&category).cloned().unwrap_or_default();
489 types.sort_by_key(|(left_name, _)| *left_name);
490 if types.is_empty() {
491 continue;
492 }
493
494 let mut code = String::new();
495 code.push_str(&format!(
496 "//! Field metadata for {}\n\nuse phf::{{phf_map, Map}};\nuse super::*;\n\n",
497 category.doc_label()
498 ));
499
500 let mut category_consts = std::collections::HashSet::new();
501
502 for (type_name, type_metadata) in &types {
503 let sanitized_name: String = type_name
504 .chars()
505 .map(|c| if c.is_alphanumeric() { c } else { '_' })
506 .collect();
507 let const_name = format!("{}_FIELDS", sanitized_name.to_uppercase());
508
509 if all_generated_consts.contains(&const_name) {
511 continue;
512 }
513
514 if category_consts.contains(&const_name) {
515 continue;
516 }
517
518 generate_type_map(&mut code, type_name, type_metadata);
519 category_consts.insert(const_name.clone());
520 all_generated_consts.insert(const_name);
521 }
522
523 let filename = format!("{}.rs", category.module_name());
524 files.insert(filename, code);
525 }
526
527 let mut mod_code = String::new();
529 mod_code.push_str(
530 r#"//! FHIR type metadata
531//!
532//! This module provides compile-time metadata about FHIR types, enabling
533//! path resolution like "Patient.name.given" -> FhirPrimitiveType::String.
534//!
535//! Generated automatically - do not edit manually.
536
537pub use phf;
538use phf::{phf_map, Map};
539
540"#,
541 );
542
543 for &category in &category_order {
545 let filename = format!("{}.rs", category.module_name());
546 if files.contains_key(&filename) {
547 mod_code.push_str(&format!("mod {};\n", category.module_name()));
548 mod_code.push_str(&format!("pub use {}::*;\n", category.module_name()));
549 }
550 }
551
552 mod_code.push('\n');
553
554 mod_code.push_str(
556 r#"/// FHIR primitive types
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
558pub enum FhirPrimitiveType {
559 Boolean,
560 Integer,
561 String,
562 Date,
563 DateTime,
564 Instant,
565 Time,
566 Decimal,
567 Uri,
568 Url,
569 Canonical,
570 Code,
571 Oid,
572 Id,
573 Markdown,
574 Base64Binary,
575 UnsignedInt,
576 PositiveInt,
577}
578
579"#,
580 );
581
582 mod_code.push_str(
584 r#"/// FHIR field type (primitive, complex, reference, or backbone element)
585#[derive(Debug, Clone, PartialEq, Eq)]
586pub enum FhirFieldType {
587 Primitive(FhirPrimitiveType),
588 Complex(&'static str),
589 Reference,
590 BackboneElement(&'static str),
591}
592
593"#,
594 );
595
596 mod_code.push_str(
598 r#"/// Information about a field in a FHIR resource or datatype
599#[derive(Debug, Clone)]
600pub struct FieldInfo {
601 pub field_type: FhirFieldType,
602 pub min: u32,
603 pub max: Option<u32>,
604 pub is_choice_type: bool,
605}
606
607"#,
608 );
609
610 generate_registry_map(&mut mod_code, ®istry.types, &all_generated_consts);
612
613 mod_code.push_str(
614 r#"
615/// Get field information for a specific field in a type
616pub fn get_field_info(type_name: &str, field_name: &str) -> Option<&'static FieldInfo> {
617 FHIR_TYPE_REGISTRY
618 .get(type_name)
619 .and_then(|fields| fields.get(field_name))
620}
621
622/// Resolve a nested path like "Patient.name.given" to its field type
623pub fn resolve_path(path: &str) -> Option<&'static FhirFieldType> {
624 let parts: Vec<&str> = path.split('.').collect();
625 if parts.is_empty() {
626 return None;
627 }
628
629 let mut current_type_name = parts[0];
630
631 for (idx, &field_name) in parts[1..].iter().enumerate() {
632 let field_info = get_field_info(current_type_name, field_name)?;
633
634 // If this is the last field, return its type
635 if idx == parts.len() - 2 {
636 return Some(&field_info.field_type);
637 }
638
639 // Otherwise, navigate to the next type
640 match &field_info.field_type {
641 FhirFieldType::Complex(type_name) | FhirFieldType::BackboneElement(type_name) => {
642 current_type_name = type_name;
643 }
644 _ => return None, // Can't navigate further
645 }
646 }
647
648 None
649}
650"#,
651 );
652
653 files.insert("mod.rs".to_string(), mod_code);
654
655 files
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn test_extract_field_name() {
664 assert_eq!(
665 extract_field_name("Patient.birthDate", "Patient"),
666 Some("birthDate".to_string())
667 );
668
669 assert_eq!(
670 extract_field_name("Patient.name.given", "Patient"),
671 Some("name".to_string())
672 );
673
674 assert_eq!(extract_field_name("Patient", "Patient"), None);
675 }
676
677 #[test]
678 fn test_determine_field_type() {
679 assert_eq!(
680 determine_field_type("date"),
681 FhirFieldType::Primitive(FhirPrimitiveType::Date)
682 );
683
684 assert_eq!(
685 determine_field_type("string"),
686 FhirFieldType::Primitive(FhirPrimitiveType::String)
687 );
688
689 assert_eq!(determine_field_type("Reference"), FhirFieldType::Reference);
690
691 assert!(matches!(
692 determine_field_type("HumanName"),
693 FhirFieldType::Complex(_)
694 ));
695 }
696}