Skip to main content

rh_codegen/generators/
metadata_generator.rs

1//! Metadata generation for FHIR types
2//!
3//! This module generates metadata module files containing type information for all FHIR resources
4//! and datatypes. The metadata is split by category (resources, datatypes, primitives, profiles)
5//! to improve incremental compile times.
6//!
7//! This metadata enables runtime path resolution like "Patient.name.given" -> string.
8
9use crate::fhir_types::{ElementDefinition, StructureDefinition};
10use crate::metadata::{
11    FhirFieldType, FhirPrimitiveType, FieldInfo, MetadataRegistry, TypeMetadata,
12};
13use std::collections::HashMap;
14
15/// Category for partitioning metadata types into separate files
16#[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
47/// Classify a StructureDefinition into a metadata category
48fn 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
84/// Build metadata registry from StructureDefinitions
85pub 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            // Only add if not already present (avoid duplicates)
91            if !registry.types.contains_key(&type_metadata.name) {
92                registry.add_type(type_metadata);
93            }
94        }
95    }
96
97    registry
98}
99
100/// Extract metadata from a single StructureDefinition
101fn 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    // Get the snapshot elements
106    let snapshot = structure_def.snapshot.as_ref()?;
107    let elements = &snapshot.element;
108
109    // Skip the first element (it's the resource itself, e.g., "Patient")
110    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
124/// Extract field name from element path (e.g., "Patient.birthDate" -> "birthDate")
125fn 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    // Only take the immediate child field (not nested paths like "name.given")
135    // We'll handle those through recursive resolution
136    let field_name = field_path.split('.').next()?;
137
138    Some(field_name.to_string())
139}
140
141/// Extract field information from an ElementDefinition
142fn 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    // Get cardinality
150    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    // Determine if this is a choice type (has [x] suffix in name)
160    let is_choice_type = element.path.contains("[x]");
161
162    // Collect all types (for choice types)
163    let choice_types: Vec<String> = element_types
164        .iter()
165        .filter_map(|et| et.code.clone())
166        .collect();
167
168    // Use the first type as the primary field type
169    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
181/// Determine the FhirFieldType from a FHIR type code string
182fn determine_field_type(type_code: &str) -> FhirFieldType {
183    // Check if it's a primitive type
184    if let Some(primitive) = FhirPrimitiveType::from_fhir_type(type_code) {
185        return FhirFieldType::Primitive(primitive);
186    }
187
188    // Check for Reference
189    if type_code == "Reference" {
190        return FhirFieldType::Reference;
191    }
192
193    // Check for BackboneElement (typically internal structures)
194    if type_code == "BackboneElement" {
195        return FhirFieldType::BackboneElement(type_code.to_string());
196    }
197
198    // Otherwise it's a complex type
199    FhirFieldType::Complex(type_code.to_string())
200}
201
202/// Generate the metadata.rs file content
203pub fn generate_metadata_code(registry: &MetadataRegistry) -> String {
204    let mut code = String::new();
205
206    // File header and imports
207    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    // Generate FhirPrimitiveType enum
221    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    // Generate FhirFieldType enum
249    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    // Generate FieldInfo struct
263    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    // Track generated const names to avoid duplicates
277    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    // Generate phf maps for each type (skip duplicates)
283    for (type_name, type_metadata) in sorted_types {
284        // Create sanitized const name
285        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        // Skip if already generated
292        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 the main registry (only include types that were generated)
301    generate_registry_map(&mut code, &registry.types, &generated_consts);
302
303    // Generate helper functions
304    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
347/// Generate a phf map for a single type's fields
348fn generate_type_map(code: &mut String, type_name: &str, type_metadata: &TypeMetadata) {
349    // Sanitize type name to create valid Rust identifier
350    // Replace any non-alphanumeric characters with underscores
351    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        // Generate field_type
370        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        // Generate cardinality
391        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        // Generate is_choice_type
401        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
412/// Generate the main registry map
413fn 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
441/// Generate split metadata files organized by category (resources, datatypes, primitives, profiles).
442///
443/// Returns a map from filename (relative to metadata dir) to file content.
444/// Categories: resources.rs, datatypes.rs, primitives.rs, profiles.rs, other.rs, mod.rs
445pub fn generate_metadata_code_split(
446    registry: &MetadataRegistry,
447    structure_defs: &[StructureDefinition],
448) -> HashMap<String, String> {
449    let mut files = HashMap::new();
450
451    // Build name -> category mapping from structure definitions (first definition wins for duplicates)
452    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    // Partition types by category
460    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    // Track all generated const names across categories for the main registry
476    let mut all_generated_consts = std::collections::HashSet::new();
477
478    // Generate each category file
479    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            // Skip if already generated in a previous category (avoids ambiguous re-exports)
510            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    // Generate mod.rs that re-exports everything and contains the main registry + helper functions
528    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    // Declare sub-modules
544    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    // Generate FhirPrimitiveType enum in mod.rs (used by all categories)
555    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    // Generate FhirFieldType enum
583    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    // Generate FieldInfo struct
597    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 the main registry that pulls from all categories
611    generate_registry_map(&mut mod_code, &registry.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}