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()
72                    } else if super_trait.ends_with("Mutators") {
73                        super_trait.strip_suffix("Mutators").unwrap()
74                    } else if super_trait.ends_with("Existence") {
75                        super_trait.strip_suffix("Existence").unwrap()
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                    // Check if the type actually exists in the TypeRegistry before generating an import
142                    if TypeRegistry::get_classification(type_name).is_some() {
143                        // Get the correct import path for this type
144                        let import_path = Self::get_import_path_for_type(type_name);
145                        imports.insert(import_path);
146                    }
147                    // If the type doesn't exist in the registry, don't generate an import
148                    // This prevents importing non-existent nested structures
149                }
150            }
151            RustType::Option(inner) => {
152                Self::collect_custom_types_from_type(
153                    inner,
154                    imports,
155                    current_struct_name,
156                    structs_in_file,
157                );
158            }
159            RustType::Vec(inner) => {
160                Self::collect_custom_types_from_type(
161                    inner,
162                    imports,
163                    current_struct_name,
164                    structs_in_file,
165                );
166            }
167            RustType::Box(inner) => {
168                Self::collect_custom_types_from_type(
169                    inner,
170                    imports,
171                    current_struct_name,
172                    structs_in_file,
173                );
174            }
175            RustType::Slice(inner) => {
176                Self::collect_custom_types_from_type(
177                    inner,
178                    imports,
179                    current_struct_name,
180                    structs_in_file,
181                );
182            }
183            RustType::Reference(name) => {
184                // Handle FHIR primitive types (like StringType, BooleanType) - these need imports
185                if Self::is_fhir_primitive_type(name) {
186                    let import_path = Self::get_fhir_primitive_import_path(name);
187                    imports.insert(import_path);
188                }
189                // Only add import for other types if it's not a Rust primitive type, not the current struct, and not in the same file
190                else if !Self::is_primitive_type(name)
191                    && name != current_struct_name
192                    && !structs_in_file.contains(name)
193                {
194                    // Check if the type actually exists in the TypeRegistry before generating an import
195                    if TypeRegistry::get_classification(name).is_some() {
196                        let import_path = Self::get_import_path_for_type(name);
197                        imports.insert(import_path);
198                    }
199                    // If the type doesn't exist in the registry, don't generate an import
200                    // This prevents importing non-existent nested structures
201                }
202            }
203            // Primitive types don't need imports
204            RustType::String | RustType::Integer | RustType::Boolean | RustType::Float => {}
205        }
206    }
207
208    /// Determine the correct import path for a given type name
209    pub fn get_import_path_for_type(type_name: &str) -> String {
210        // First try the TypeRegistry for accurate classification based on StructureDefinition
211        TypeRegistry::get_import_path_for_type(type_name)
212    }
213
214    /// Check if a type is a FHIR resource type
215    pub fn is_fhir_resource_type(type_name: &str) -> bool {
216        NamingManager::is_fhir_resource(type_name)
217    }
218
219    /// Check if a type is a FHIR trait type (ends with Accessors, Mutators, or Existence)
220    pub fn is_fhir_trait_type(type_name: &str) -> bool {
221        type_name.ends_with("Accessors")
222            || type_name.ends_with("Mutators")
223            || type_name.ends_with("Existence")
224    }
225
226    /// Check if a type name represents a known FHIR data type
227    pub fn is_fhir_datatype(type_name: &str) -> bool {
228        NamingManager::is_fhir_datatype(type_name)
229    }
230
231    /// Check if a type is a FHIR primitive type
232    pub fn is_fhir_primitive_type(type_name: &str) -> bool {
233        // FHIR primitive types that have extensions - matching actual generated type names
234        matches!(
235            type_name,
236            "StringType"
237                | "BooleanType"
238                | "IntegerType"
239                | "DecimalType"
240                | "UriType"
241                | "UrlType"
242                | "CanonicalType"
243                | "OidType"
244                | "UuidType"
245                | "InstantType"
246                | "DateType"
247                | "DateTimeType"
248                | "TimeType"
249                | "CodeType"
250                | "IdType"
251                | "MarkdownType"
252                | "Base64BinaryType"
253                | "UnsignedIntType"
254                | "PositiveIntType"
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            "XhtmlType" => "xhtml",
303            _ => "unknown",
304        };
305        format!("crate::primitives::{module_name}::{type_name}")
306    }
307
308    /// Check if a type is a generated trait
309    pub fn is_generated_trait(type_name: &str) -> bool {
310        // Traits are typically generated for base types or common interfaces
311        let lower_name = type_name.to_lowercase();
312        lower_name.ends_with("trait")
313            || lower_name.ends_with("accessors")
314            || lower_name.ends_with("mutators")
315            || lower_name.ends_with("helpers")
316            || matches!(
317                lower_name.as_str(),
318                "resourcetrait"
319                    | "domainresourcetrait"
320                    | "backboneelementtrait"
321                    | "elementtrait"
322                    | "metadataresourcetrait"
323                    | "resourceaccessors"
324                    | "domainresourceaccessors"
325                    | "backboneelementaccessors"
326                    | "elementaccessors"
327                    | "metadataresourceaccessors"
328            )
329    }
330
331    /// Check if a type name represents a primitive Rust type
332    pub fn is_primitive_type(type_name: &str) -> bool {
333        matches!(
334            type_name,
335            "String"
336                | "&str"
337                | "str"
338                | "i32"
339                | "u32"
340                | "i64"
341                | "u64"
342                | "f32"
343                | "f64"
344                | "bool"
345                | "usize"
346                | "isize"
347                | "Self" // Built-in Rust keyword, should not be imported
348        )
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_import_classification() {
358        // Test resource classification
359        assert!(ImportManager::is_fhir_resource_type("DomainResource"));
360        assert!(ImportManager::is_fhir_resource_type("Patient"));
361        assert!(ImportManager::is_fhir_resource_type("ActivityDefinition"));
362        assert!(!ImportManager::is_fhir_resource_type("Identifier"));
363
364        // Test datatype classification
365        assert!(ImportManager::is_fhir_datatype("Identifier"));
366        assert!(ImportManager::is_fhir_datatype("CodeableConcept"));
367        assert!(ImportManager::is_fhir_datatype("Reference"));
368        assert!(!ImportManager::is_fhir_datatype("DomainResource"));
369
370        // Test import path generation
371        assert_eq!(
372            ImportManager::get_import_path_for_type("DomainResource"),
373            "crate::resources::domain_resource::DomainResource"
374        );
375        assert_eq!(
376            ImportManager::get_import_path_for_type("Identifier"),
377            "crate::datatypes::identifier::Identifier"
378        );
379        assert_eq!(
380            ImportManager::get_import_path_for_type("PublicationStatus"),
381            "crate::bindings::publication_status::PublicationStatus"
382        );
383    }
384
385    #[test]
386    fn test_primitive_type_detection() {
387        assert!(ImportManager::is_primitive_type("String"));
388        assert!(ImportManager::is_primitive_type("i32"));
389        assert!(ImportManager::is_primitive_type("bool"));
390        assert!(!ImportManager::is_primitive_type("Patient"));
391        assert!(!ImportManager::is_primitive_type("Identifier"));
392    }
393
394    #[test]
395    fn test_nested_structure_detection() {
396        // Test nested structure import path detection with longest prefix matching
397        assert_eq!(
398            ImportManager::get_import_path_for_type("EvidenceVariableCharacteristic"),
399            "crate::resources::evidence_variable::EvidenceVariableCharacteristic"
400        );
401        assert_eq!(
402            ImportManager::get_import_path_for_type("MeasureReportGroup"),
403            "crate::resources::measure_report::MeasureReportGroup"
404        );
405        assert_eq!(
406            ImportManager::get_import_path_for_type("AccountCoverage"),
407            "crate::resources::account::AccountCoverage"
408        );
409        assert_eq!(
410            ImportManager::get_import_path_for_type("AccountGuarantor"),
411            "crate::resources::account::AccountGuarantor"
412        );
413        assert_eq!(
414            ImportManager::get_import_path_for_type("BundleEntry"),
415            "crate::resources::bundle::BundleEntry"
416        );
417        assert_eq!(
418            ImportManager::get_import_path_for_type("ImplementationGuideGlobal"),
419            "crate::resources::implementation_guide::ImplementationGuideGlobal"
420        );
421
422        // Test ConditionStage specifically - this is the failing case
423        assert_eq!(
424            ImportManager::get_import_path_for_type("ConditionStage"),
425            "crate::resources::condition::ConditionStage"
426        );
427
428        // Test that non-nested structures still work correctly
429        assert_eq!(
430            ImportManager::get_import_path_for_type("Patient"),
431            "crate::resources::patient::Patient"
432        );
433        assert_eq!(
434            ImportManager::get_import_path_for_type("Identifier"),
435            "crate::datatypes::identifier::Identifier"
436        );
437    }
438}