1use crate::fhir_types::{ElementDefinition, StructureDefinition};
7use crate::metadata::{
8 FhirFieldType, FhirPrimitiveType, FieldInfo, MetadataRegistry, TypeMetadata,
9};
10use std::collections::HashMap;
11
12pub 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 if !registry.types.contains_key(&type_metadata.name) {
20 registry.add_type(type_metadata);
21 }
22 }
23 }
24
25 registry
26}
27
28fn 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 let snapshot = structure_def.snapshot.as_ref()?;
35 let elements = &snapshot.element;
36
37 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
52fn 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 let field_name = field_path.split('.').next()?;
65
66 Some(field_name.to_string())
67}
68
69fn 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 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 let is_choice_type = element.path.contains("[x]");
89
90 let choice_types: Vec<String> = element_types
92 .iter()
93 .filter_map(|et| et.code.clone())
94 .collect();
95
96 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
109fn determine_field_type(type_code: &str) -> FhirFieldType {
111 if let Some(primitive) = FhirPrimitiveType::from_fhir_type(type_code) {
113 return FhirFieldType::Primitive(primitive);
114 }
115
116 if type_code == "Reference" {
118 return FhirFieldType::Reference;
119 }
120
121 if type_code == "BackboneElement" {
123 return FhirFieldType::BackboneElement(type_code.to_string());
124 }
125
126 FhirFieldType::Complex(type_code.to_string())
128}
129
130pub fn generate_metadata_code(registry: &MetadataRegistry) -> String {
132 let mut code = String::new();
133
134 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 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 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 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 let mut generated_consts = std::collections::HashSet::new();
206
207 for (type_name, type_metadata) in ®istry.types {
209 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 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_registry_map(&mut code, ®istry.types, &generated_consts);
227
228 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
272fn generate_type_map(code: &mut String, type_name: &str, type_metadata: &TypeMetadata) {
274 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 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 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 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
333fn 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 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 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}