Skip to main content

rh_codegen/generators/
metadata_generator.rs

1//! Metadata generation for FHIR types
2//!
3//! This module generates a metadata.rs file containing type information for all FHIR resources
4//! and datatypes. This metadata enables runtime path resolution like "Patient.name.given" -> string.
5
6use crate::fhir_types::{ElementDefinition, StructureDefinition};
7use crate::metadata::{
8    FhirFieldType, FhirPrimitiveType, FieldInfo, MetadataRegistry, TypeMetadata,
9};
10use std::collections::HashMap;
11
12/// Build metadata registry from StructureDefinitions
13pub fn build_metadata_registry(structure_defs: &[StructureDefinition]) -> MetadataRegistry {
14    let mut registry = MetadataRegistry::new();
15
16    for structure_def in structure_defs {
17        if let Some(type_metadata) = extract_type_metadata(structure_def) {
18            // Only add if not already present (avoid duplicates)
19            if !registry.types.contains_key(&type_metadata.name) {
20                registry.add_type(type_metadata);
21            }
22        }
23    }
24
25    registry
26}
27
28/// Extract metadata from a single StructureDefinition
29fn extract_type_metadata(structure_def: &StructureDefinition) -> Option<TypeMetadata> {
30    let type_name = structure_def.name.as_str();
31    let mut fields = HashMap::new();
32
33    // Get the snapshot elements
34    let snapshot = structure_def.snapshot.as_ref()?;
35    let elements = &snapshot.element;
36
37    // Skip the first element (it's the resource itself, e.g., "Patient")
38    for element in elements.iter().skip(1) {
39        if let Some(field_info) = extract_field_info(element, type_name) {
40            if let Some(field_name) = extract_field_name(&element.path, type_name) {
41                fields.insert(field_name, field_info);
42            }
43        }
44    }
45
46    Some(TypeMetadata {
47        name: type_name.to_string(),
48        fields,
49    })
50}
51
52/// Extract field name from element path (e.g., "Patient.birthDate" -> "birthDate")
53fn extract_field_name(path: &str, type_name: &str) -> Option<String> {
54    let prefix = format!("{type_name}.");
55
56    if !path.starts_with(&prefix) {
57        return None;
58    }
59
60    let field_path = &path[prefix.len()..];
61
62    // Only take the immediate child field (not nested paths like "name.given")
63    // We'll handle those through recursive resolution
64    let field_name = field_path.split('.').next()?;
65
66    Some(field_name.to_string())
67}
68
69/// Extract field information from an ElementDefinition
70fn extract_field_info(element: &ElementDefinition, _type_name: &str) -> Option<FieldInfo> {
71    let element_types = element.element_type.as_ref()?;
72
73    if element_types.is_empty() {
74        return None;
75    }
76
77    // Get cardinality
78    let min = element.min.unwrap_or(0);
79    let max = element.max.as_ref().and_then(|m| {
80        if m == "*" {
81            None
82        } else {
83            m.parse::<u32>().ok()
84        }
85    });
86
87    // Determine if this is a choice type (has [x] suffix in name)
88    let is_choice_type = element.path.contains("[x]");
89
90    // Collect all types (for choice types)
91    let choice_types: Vec<String> = element_types
92        .iter()
93        .filter_map(|et| et.code.clone())
94        .collect();
95
96    // Use the first type as the primary field type
97    let primary_type_code = element_types[0].code.as_ref()?;
98    let field_type = determine_field_type(primary_type_code);
99
100    Some(FieldInfo {
101        field_type,
102        min,
103        max,
104        is_choice_type,
105        choice_types,
106    })
107}
108
109/// Determine the FhirFieldType from a FHIR type code string
110fn determine_field_type(type_code: &str) -> FhirFieldType {
111    // Check if it's a primitive type
112    if let Some(primitive) = FhirPrimitiveType::from_fhir_type(type_code) {
113        return FhirFieldType::Primitive(primitive);
114    }
115
116    // Check for Reference
117    if type_code == "Reference" {
118        return FhirFieldType::Reference;
119    }
120
121    // Check for BackboneElement (typically internal structures)
122    if type_code == "BackboneElement" {
123        return FhirFieldType::BackboneElement(type_code.to_string());
124    }
125
126    // Otherwise it's a complex type
127    FhirFieldType::Complex(type_code.to_string())
128}
129
130/// Generate the metadata.rs file content
131pub fn generate_metadata_code(registry: &MetadataRegistry) -> String {
132    let mut code = String::new();
133
134    // File header and imports
135    code.push_str(
136        r#"//! FHIR type metadata
137//!
138//! This module provides compile-time metadata about FHIR types, enabling
139//! path resolution like "Patient.name.given" -> FhirPrimitiveType::String.
140//!
141//! Generated automatically - do not edit manually.
142
143use phf::{phf_map, Map};
144
145"#,
146    );
147
148    // Generate FhirPrimitiveType enum
149    code.push_str(
150        r#"/// FHIR primitive types
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152pub enum FhirPrimitiveType {
153    Boolean,
154    Integer,
155    String,
156    Date,
157    DateTime,
158    Instant,
159    Time,
160    Decimal,
161    Uri,
162    Url,
163    Canonical,
164    Code,
165    Oid,
166    Id,
167    Markdown,
168    Base64Binary,
169    UnsignedInt,
170    PositiveInt,
171}
172
173"#,
174    );
175
176    // Generate FhirFieldType enum
177    code.push_str(
178        r#"/// FHIR field type (primitive, complex, reference, or backbone element)
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum FhirFieldType {
181    Primitive(FhirPrimitiveType),
182    Complex(&'static str),
183    Reference,
184    BackboneElement(&'static str),
185}
186
187"#,
188    );
189
190    // Generate FieldInfo struct
191    code.push_str(
192        r#"/// Information about a field in a FHIR resource or datatype
193#[derive(Debug, Clone)]
194pub struct FieldInfo {
195    pub field_type: FhirFieldType,
196    pub min: u32,
197    pub max: Option<u32>,
198    pub is_choice_type: bool,
199}
200
201"#,
202    );
203
204    // Track generated const names to avoid duplicates
205    let mut generated_consts = std::collections::HashSet::new();
206
207    // Generate phf maps for each type (skip duplicates)
208    for (type_name, type_metadata) in &registry.types {
209        // Create sanitized const name
210        let sanitized_name: String = type_name
211            .chars()
212            .map(|c| if c.is_alphanumeric() { c } else { '_' })
213            .collect();
214        let const_name = format!("{}_FIELDS", sanitized_name.to_uppercase());
215
216        // Skip if already generated
217        if generated_consts.contains(&const_name) {
218            continue;
219        }
220
221        generate_type_map(&mut code, type_name, type_metadata);
222        generated_consts.insert(const_name);
223    }
224
225    // Generate the main registry (only include types that were generated)
226    generate_registry_map(&mut code, &registry.types, &generated_consts);
227
228    // Generate helper functions
229    code.push_str(
230        r#"
231/// Get field information for a specific field in a type
232pub fn get_field_info(type_name: &str, field_name: &str) -> Option<&'static FieldInfo> {
233    FHIR_TYPE_REGISTRY
234        .get(type_name)
235        .and_then(|fields| fields.get(field_name))
236}
237
238/// Resolve a nested path like "Patient.name.given" to its field type
239pub fn resolve_path(path: &str) -> Option<&'static FhirFieldType> {
240    let parts: Vec<&str> = path.split('.').collect();
241    if parts.is_empty() {
242        return None;
243    }
244
245    let mut current_type_name = parts[0];
246    
247    for (idx, &field_name) in parts[1..].iter().enumerate() {
248        let field_info = get_field_info(current_type_name, field_name)?;
249        
250        // If this is the last field, return its type
251        if idx == parts.len() - 2 {
252            return Some(&field_info.field_type);
253        }
254        
255        // Otherwise, navigate to the next type
256        match &field_info.field_type {
257            FhirFieldType::Complex(type_name) | FhirFieldType::BackboneElement(type_name) => {
258                current_type_name = type_name;
259            }
260            _ => return None, // Can't navigate further
261        }
262    }
263    
264    None
265}
266"#,
267    );
268
269    code
270}
271
272/// Generate a phf map for a single type's fields
273fn generate_type_map(code: &mut String, type_name: &str, type_metadata: &TypeMetadata) {
274    // Sanitize type name to create valid Rust identifier
275    // Replace any non-alphanumeric characters with underscores
276    let sanitized_name: String = type_name
277        .chars()
278        .map(|c| if c.is_alphanumeric() { c } else { '_' })
279        .collect();
280    let const_name = format!("{}_FIELDS", sanitized_name.to_uppercase());
281
282    code.push_str(&format!("/// Field metadata for {type_name}\n"));
283    code.push_str(&format!(
284        "pub static {const_name}: Map<&'static str, FieldInfo> = phf_map! {{\n"
285    ));
286
287    for (field_name, field_info) in &type_metadata.fields {
288        code.push_str(&format!("    \"{field_name}\" => FieldInfo {{\n"));
289
290        // Generate field_type
291        code.push_str("        field_type: ");
292        match &field_info.field_type {
293            FhirFieldType::Primitive(prim) => {
294                code.push_str(&format!(
295                    "FhirFieldType::Primitive(FhirPrimitiveType::{})",
296                    prim.variant_name()
297                ));
298            }
299            FhirFieldType::Complex(name) => {
300                code.push_str(&format!("FhirFieldType::Complex(\"{name}\")"));
301            }
302            FhirFieldType::Reference => {
303                code.push_str("FhirFieldType::Reference");
304            }
305            FhirFieldType::BackboneElement(name) => {
306                code.push_str(&format!("FhirFieldType::BackboneElement(\"{name}\")"));
307            }
308        }
309        code.push_str(",\n");
310
311        // Generate cardinality
312        code.push_str(&format!("        min: {},\n", field_info.min));
313        code.push_str("        max: ");
314        if let Some(max) = field_info.max {
315            code.push_str(&format!("Some({max})"));
316        } else {
317            code.push_str("None");
318        }
319        code.push_str(",\n");
320
321        // Generate is_choice_type
322        code.push_str(&format!(
323            "        is_choice_type: {},\n",
324            field_info.is_choice_type
325        ));
326
327        code.push_str("    },\n");
328    }
329
330    code.push_str("};\n\n");
331}
332
333/// Generate the main registry map
334fn generate_registry_map(
335    code: &mut String,
336    types: &HashMap<String, TypeMetadata>,
337    generated_consts: &std::collections::HashSet<String>,
338) {
339    code.push_str("/// Main FHIR type registry mapping type names to their field metadata\n");
340    code.push_str(
341        "pub static FHIR_TYPE_REGISTRY: Map<&'static str, &'static Map<&'static str, FieldInfo>> = phf_map! {\n",
342    );
343
344    for type_name in types.keys() {
345        // Sanitize type name to create valid Rust identifier
346        // Replace any non-alphanumeric characters with underscores
347        let sanitized_name: String = type_name
348            .chars()
349            .map(|c| if c.is_alphanumeric() { c } else { '_' })
350            .collect();
351        let const_name = format!("{}_FIELDS", sanitized_name.to_uppercase());
352
353        // Only include if the const was actually generated
354        if generated_consts.contains(&const_name) {
355            code.push_str(&format!("    \"{type_name}\" => &{const_name},\n"));
356        }
357    }
358
359    code.push_str("};\n");
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_extract_field_name() {
368        assert_eq!(
369            extract_field_name("Patient.birthDate", "Patient"),
370            Some("birthDate".to_string())
371        );
372
373        assert_eq!(
374            extract_field_name("Patient.name.given", "Patient"),
375            Some("name".to_string())
376        );
377
378        assert_eq!(extract_field_name("Patient", "Patient"), None);
379    }
380
381    #[test]
382    fn test_determine_field_type() {
383        assert_eq!(
384            determine_field_type("date"),
385            FhirFieldType::Primitive(FhirPrimitiveType::Date)
386        );
387
388        assert_eq!(
389            determine_field_type("string"),
390            FhirFieldType::Primitive(FhirPrimitiveType::String)
391        );
392
393        assert_eq!(determine_field_type("Reference"), FhirFieldType::Reference);
394
395        assert!(matches!(
396            determine_field_type("HumanName"),
397            FhirFieldType::Complex(_)
398        ));
399    }
400}