Skip to main content

rh_codegen/generators/
import_manager.rs

1//! Import management utilities
2//!
3//! This module handles the determination of import paths for FHIR types
4//! and collection of custom types for import generation.
5
6use crate::generators::naming_manager::NamingManager;
7use crate::generators::type_registry::TypeRegistry;
8use crate::rust_types::{RustStruct, RustType};
9use std::collections::HashSet;
10
11/// Utility struct for managing imports and type classifications
12pub struct ImportManager;
13
14impl ImportManager {
15    /// Collect all custom types referenced by a struct and add them to the imports set
16    pub fn collect_custom_types_from_struct(
17        rust_struct: &RustStruct,
18        imports: &mut HashSet<String>,
19        structs_in_file: &HashSet<String>,
20    ) {
21        // Add import for base type if present
22        if let Some(base_def) = &rust_struct.base_definition {
23            let base_type = base_def.split('/').next_back().unwrap_or(base_def);
24            // Only add import if it's not a primitive type, not the current struct, and not in the same file
25            if !Self::is_primitive_type(base_type)
26                && base_type != rust_struct.name
27                && !structs_in_file.contains(base_type)
28            {
29                // For base definitions, ensure proper struct name casing but only for
30                // names that are clearly in all lowercase (like "vitalsigns")
31                let proper_base_type = if base_type
32                    .chars()
33                    .all(|c| c.is_lowercase() || c.is_numeric())
34                {
35                    // Convert all-lowercase names to PascalCase (e.g., "vitalsigns" -> "Vitalsigns")
36                    crate::naming::Naming::capitalize_first(base_type)
37                } else {
38                    // Keep names that already have proper casing (e.g., "BackboneElement")
39                    base_type.to_string()
40                };
41                let import_path = Self::get_import_path_for_type(&proper_base_type);
42                imports.insert(import_path);
43            }
44        }
45
46        // Collect custom types from all fields
47        for field in &rust_struct.fields {
48            Self::collect_custom_types_from_type(
49                &field.field_type,
50                imports,
51                &rust_struct.name,
52                structs_in_file,
53            );
54        }
55    }
56
57    /// Collect all custom types referenced by a trait and add them to the imports set
58    pub fn collect_custom_types_from_trait(
59        rust_trait: &crate::rust_types::RustTrait,
60        imports: &mut HashSet<String>,
61    ) {
62        // Add imports for super traits
63        for super_trait in &rust_trait.super_traits {
64            if !Self::is_primitive_type(super_trait) {
65                // For super traits that are FHIR trait types (Accessors, Mutators, Existence)
66                // or FHIR resources, import from the traits module
67                let import_path = if Self::is_fhir_trait_type(super_trait) {
68                    // For trait types like "DomainResourceAccessors", extract the base name
69                    // and use it for the module path: crate::traits::domain_resource::DomainResourceAccessors
70                    let base_name = if super_trait.ends_with("Accessors") {
71                        super_trait.strip_suffix("Accessors").unwrap_or(super_trait)
72                    } else if super_trait.ends_with("Mutators") {
73                        super_trait.strip_suffix("Mutators").unwrap_or(super_trait)
74                    } else if super_trait.ends_with("Existence") {
75                        super_trait.strip_suffix("Existence").unwrap_or(super_trait)
76                    } else {
77                        super_trait
78                    };
79
80                    format!(
81                        "crate::traits::{}::{}",
82                        crate::naming::Naming::to_snake_case(base_name),
83                        super_trait
84                    )
85                } else if Self::is_fhir_resource_type(super_trait) {
86                    format!(
87                        "crate::traits::{}::{}",
88                        crate::naming::Naming::to_snake_case(super_trait),
89                        super_trait
90                    )
91                } else {
92                    Self::get_import_path_for_type(super_trait)
93                };
94                imports.insert(import_path);
95            }
96        }
97
98        // Collect custom types from all trait methods
99        for method in &rust_trait.methods {
100            // Collect types from return type
101            if let Some(return_type) = &method.return_type {
102                Self::collect_custom_types_from_type(
103                    return_type,
104                    imports,
105                    &rust_trait.name,
106                    &HashSet::new(), // Traits don't have "structs in file"
107                );
108            }
109
110            // Collect types from parameters (if any)
111            for param in &method.params {
112                Self::collect_custom_types_from_type(
113                    &param.param_type,
114                    imports,
115                    &rust_trait.name,
116                    &HashSet::new(),
117                );
118            }
119        }
120    }
121
122    /// Recursively collect custom types from a RustType
123    pub fn collect_custom_types_from_type(
124        rust_type: &RustType,
125        imports: &mut HashSet<String>,
126        current_struct_name: &str,
127        structs_in_file: &HashSet<String>,
128    ) {
129        match rust_type {
130            RustType::Custom(type_name) => {
131                // Handle FHIR primitive types (like StringType, BooleanType) - these need imports
132                if Self::is_fhir_primitive_type(type_name) {
133                    let import_path = Self::get_fhir_primitive_import_path(type_name);
134                    imports.insert(import_path);
135                }
136                // Only add import for other types if it's not a Rust primitive type, not the current struct, and not in the same file
137                else if !Self::is_primitive_type(type_name)
138                    && type_name != current_struct_name
139                    && !structs_in_file.contains(type_name)
140                {
141                    // Trait files are generated separately from the resource module, so they
142                    // need imports for nested resource structs referenced in method signatures.
143                    // During parallel generation the TypeRegistry may not contain every sibling
144                    // nested struct yet, so allow NamingManager's nested-structure fallback here.
145                    if TypeRegistry::get_classification(type_name).is_some()
146                        || NamingManager::detect_nested_structure_parent(type_name).is_some()
147                    {
148                        let import_path = Self::get_import_path_for_type(type_name);
149                        imports.insert(import_path);
150                    }
151                }
152            }
153            RustType::Option(inner) => {
154                Self::collect_custom_types_from_type(
155                    inner,
156                    imports,
157                    current_struct_name,
158                    structs_in_file,
159                );
160            }
161            RustType::Vec(inner) => {
162                Self::collect_custom_types_from_type(
163                    inner,
164                    imports,
165                    current_struct_name,
166                    structs_in_file,
167                );
168            }
169            RustType::Box(inner) => {
170                Self::collect_custom_types_from_type(
171                    inner,
172                    imports,
173                    current_struct_name,
174                    structs_in_file,
175                );
176            }
177            RustType::Slice(inner) => {
178                Self::collect_custom_types_from_type(
179                    inner,
180                    imports,
181                    current_struct_name,
182                    structs_in_file,
183                );
184            }
185            RustType::Reference(name) => {
186                // Handle FHIR primitive types (like StringType, BooleanType) - these need imports
187                if Self::is_fhir_primitive_type(name) {
188                    let import_path = Self::get_fhir_primitive_import_path(name);
189                    imports.insert(import_path);
190                }
191                // Only add import for other types if it's not a Rust primitive type, not the current struct, and not in the same file
192                else if !Self::is_primitive_type(name)
193                    && name != current_struct_name
194                    && !structs_in_file.contains(name)
195                    && (TypeRegistry::get_classification(name).is_some()
196                        || NamingManager::detect_nested_structure_parent(name).is_some())
197                {
198                    let import_path = Self::get_import_path_for_type(name);
199                    imports.insert(import_path);
200                }
201            }
202            // Primitive types don't need imports
203            RustType::String | RustType::Integer | RustType::Boolean | RustType::Float => {}
204        }
205    }
206
207    /// Determine the correct import path for a given type name
208    pub fn get_import_path_for_type(type_name: &str) -> String {
209        // First try the TypeRegistry for accurate classification based on StructureDefinition
210        TypeRegistry::get_import_path_for_type(type_name)
211    }
212
213    /// Check if a type is a FHIR resource type
214    pub fn is_fhir_resource_type(type_name: &str) -> bool {
215        NamingManager::is_fhir_resource(type_name)
216    }
217
218    /// Check if a type is a FHIR trait type (ends with Accessors, Mutators, or Existence)
219    pub fn is_fhir_trait_type(type_name: &str) -> bool {
220        type_name.ends_with("Accessors")
221            || type_name.ends_with("Mutators")
222            || type_name.ends_with("Existence")
223    }
224
225    /// Check if a type name represents a known FHIR data type
226    pub fn is_fhir_datatype(type_name: &str) -> bool {
227        NamingManager::is_fhir_datatype(type_name)
228    }
229
230    /// Check if a type is a FHIR primitive type
231    pub fn is_fhir_primitive_type(type_name: &str) -> bool {
232        // FHIR primitive types that have extensions - matching actual generated type names
233        matches!(
234            type_name,
235            "StringType"
236                | "BooleanType"
237                | "IntegerType"
238                | "DecimalType"
239                | "UriType"
240                | "UrlType"
241                | "CanonicalType"
242                | "OidType"
243                | "UuidType"
244                | "InstantType"
245                | "DateType"
246                | "DateTimeType"
247                | "TimeType"
248                | "CodeType"
249                | "IdType"
250                | "MarkdownType"
251                | "Base64BinaryType"
252                | "UnsignedIntType"
253                | "PositiveIntType"
254                | "Integer64Type"
255                | "XhtmlType"
256                // Also handle the non-Type variants that appear in trait methods
257                | "String"
258                | "Boolean"
259                | "Integer"
260                | "Decimal"
261                | "Uri"
262                | "Url"
263                | "Canonical"
264                | "Oid"
265                | "Uuid"
266                | "Instant"
267                | "Date"
268                | "DateTime"
269                | "Time"
270                | "Code"
271                | "Id"
272                | "Markdown"
273                | "Base64Binary"
274                | "UnsignedInt"
275                | "PositiveInt"
276                | "Xhtml"
277        )
278    }
279
280    /// Get the import path for a FHIR primitive type
281    pub fn get_fhir_primitive_import_path(type_name: &str) -> String {
282        let module_name = match type_name {
283            "StringType" => "string",
284            "BooleanType" => "boolean",
285            "IntegerType" => "integer",
286            "DecimalType" => "decimal",
287            "UriType" => "uri",
288            "UrlType" => "url",
289            "CanonicalType" => "canonical",
290            "OidType" => "oid",
291            "UuidType" => "uuid",
292            "InstantType" => "instant",
293            "DateType" => "date",
294            "DateTimeType" => "date_time",
295            "TimeType" => "time",
296            "CodeType" => "code",
297            "IdType" => "id",
298            "MarkdownType" => "markdown",
299            "Base64BinaryType" => "base64binary",
300            "UnsignedIntType" => "unsigned_int",
301            "PositiveIntType" => "positive_int",
302            "Integer64Type" => "integer64",
303            "XhtmlType" => "xhtml",
304            _ => "unknown",
305        };
306        format!("crate::primitives::{module_name}::{type_name}")
307    }
308
309    /// Check if a type is a generated trait
310    pub fn is_generated_trait(type_name: &str) -> bool {
311        // Traits are typically generated for base types or common interfaces
312        let lower_name = type_name.to_lowercase();
313        lower_name.ends_with("trait")
314            || lower_name.ends_with("accessors")
315            || lower_name.ends_with("mutators")
316            || lower_name.ends_with("helpers")
317            || matches!(
318                lower_name.as_str(),
319                "resourcetrait"
320                    | "domainresourcetrait"
321                    | "backboneelementtrait"
322                    | "elementtrait"
323                    | "metadataresourcetrait"
324                    | "resourceaccessors"
325                    | "domainresourceaccessors"
326                    | "backboneelementaccessors"
327                    | "elementaccessors"
328                    | "metadataresourceaccessors"
329            )
330    }
331
332    /// Check if a type name represents a primitive Rust type
333    pub fn is_primitive_type(type_name: &str) -> bool {
334        matches!(
335            type_name,
336            "String"
337                | "&str"
338                | "str"
339                | "i32"
340                | "u32"
341                | "i64"
342                | "u64"
343                | "f32"
344                | "f64"
345                | "bool"
346                | "usize"
347                | "isize"
348                | "Self" // Built-in Rust keyword, should not be imported
349        )
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_import_classification() {
359        // Test resource classification
360        assert!(ImportManager::is_fhir_resource_type("DomainResource"));
361        assert!(ImportManager::is_fhir_resource_type("Patient"));
362        assert!(ImportManager::is_fhir_resource_type("ActivityDefinition"));
363        assert!(!ImportManager::is_fhir_resource_type("Identifier"));
364
365        // Test datatype classification
366        assert!(ImportManager::is_fhir_datatype("Identifier"));
367        assert!(ImportManager::is_fhir_datatype("CodeableConcept"));
368        assert!(ImportManager::is_fhir_datatype("Reference"));
369        assert!(!ImportManager::is_fhir_datatype("DomainResource"));
370
371        // Test import path generation
372        assert_eq!(
373            ImportManager::get_import_path_for_type("DomainResource"),
374            "crate::resources::domain_resource::DomainResource"
375        );
376        assert_eq!(
377            ImportManager::get_import_path_for_type("Identifier"),
378            "crate::datatypes::identifier::Identifier"
379        );
380        assert_eq!(
381            ImportManager::get_import_path_for_type("PublicationStatus"),
382            "crate::bindings::publication_status::PublicationStatus"
383        );
384    }
385
386    #[test]
387    fn test_primitive_type_detection() {
388        assert!(ImportManager::is_primitive_type("String"));
389        assert!(ImportManager::is_primitive_type("i32"));
390        assert!(ImportManager::is_primitive_type("bool"));
391        assert!(!ImportManager::is_primitive_type("Patient"));
392        assert!(!ImportManager::is_primitive_type("Identifier"));
393    }
394
395    #[test]
396    fn test_nested_structure_detection() {
397        // Test nested structure import path detection with longest prefix matching
398        assert_eq!(
399            ImportManager::get_import_path_for_type("EvidenceVariableCharacteristic"),
400            "crate::resources::evidence_variable::EvidenceVariableCharacteristic"
401        );
402        assert_eq!(
403            ImportManager::get_import_path_for_type("MeasureReportGroup"),
404            "crate::resources::measure_report::MeasureReportGroup"
405        );
406        assert_eq!(
407            ImportManager::get_import_path_for_type("AccountCoverage"),
408            "crate::resources::account::AccountCoverage"
409        );
410        assert_eq!(
411            ImportManager::get_import_path_for_type("AccountGuarantor"),
412            "crate::resources::account::AccountGuarantor"
413        );
414        assert_eq!(
415            ImportManager::get_import_path_for_type("BundleEntry"),
416            "crate::resources::bundle::BundleEntry"
417        );
418        assert_eq!(
419            ImportManager::get_import_path_for_type("ImplementationGuideGlobal"),
420            "crate::resources::implementation_guide::ImplementationGuideGlobal"
421        );
422
423        // Test ConditionStage specifically - this is the failing case
424        assert_eq!(
425            ImportManager::get_import_path_for_type("ConditionStage"),
426            "crate::resources::condition::ConditionStage"
427        );
428
429        // Test that non-nested structures still work correctly
430        assert_eq!(
431            ImportManager::get_import_path_for_type("Patient"),
432            "crate::resources::patient::Patient"
433        );
434        assert_eq!(
435            ImportManager::get_import_path_for_type("Identifier"),
436            "crate::datatypes::identifier::Identifier"
437        );
438    }
439}