Skip to main content

rh_codegen/generators/
primitive_generator.rs

1//! Primitive type generation functionality
2//!
3//! This module handles the generation of FHIR primitive types and their companion Element structs.
4
5use std::collections::HashMap;
6
7use crate::config::CodegenConfig;
8use crate::fhir_types::StructureDefinition;
9use crate::generators::DocumentationGenerator;
10use crate::rust_types::{RustField, RustStruct, RustType, RustTypeAlias};
11use crate::CodegenResult;
12
13/// Primitive type generator for FHIR primitive types
14pub struct PrimitiveGenerator<'a> {
15    config: &'a CodegenConfig,
16    type_cache: &'a mut HashMap<String, RustStruct>,
17}
18
19impl<'a> PrimitiveGenerator<'a> {
20    /// Create a new primitive generator
21    pub fn new(config: &'a CodegenConfig, type_cache: &'a mut HashMap<String, RustStruct>) -> Self {
22        Self { config, type_cache }
23    }
24
25    /// Generate all primitive type aliases for a combined primitives.rs file
26    pub fn generate_all_primitive_type_aliases(
27        &self,
28        primitive_structure_defs: &[StructureDefinition],
29    ) -> CodegenResult<Vec<RustTypeAlias>> {
30        let mut type_aliases = Vec::new();
31
32        for structure_def in primitive_structure_defs {
33            let type_alias = self.generate_primitive_type_alias(structure_def)?;
34            type_aliases.push(type_alias);
35        }
36
37        Ok(type_aliases)
38    }
39
40    /// Generate a type alias for primitive types
41    pub fn generate_primitive_type_alias(
42        &self,
43        structure_def: &StructureDefinition,
44    ) -> CodegenResult<RustTypeAlias> {
45        // Map FHIR primitive types to Rust types
46        let rust_primitive_type = Self::map_fhir_primitive_to_rust_type(&structure_def.name);
47
48        // Convert to PascalCase and add "Type" suffix
49        let type_alias_name = format!(
50            "{}Type",
51            Self::fhir_primitive_to_pascal_case(&structure_def.name)
52        );
53
54        // Create type alias with documentation
55        let mut type_alias = RustTypeAlias::new(type_alias_name, rust_primitive_type);
56        let doc =
57            DocumentationGenerator::generate_primitive_type_alias_documentation(structure_def);
58        type_alias = type_alias.with_doc(doc);
59
60        Ok(type_alias)
61    }
62
63    /// Convert FHIR primitive name to PascalCase, handling camelCase inputs
64    fn fhir_primitive_to_pascal_case(name: &str) -> String {
65        match name {
66            // Handle special cases that need custom conversion
67            "dateTime" => "DateTime".to_string(),
68            "positiveInt" => "PositiveInt".to_string(),
69            "unsignedInt" => "UnsignedInt".to_string(),
70            "base64Binary" => "Base64Binary".to_string(),
71            // For simple lowercase names, just capitalize
72            _ => {
73                let mut chars = name.chars();
74                match chars.next() {
75                    None => String::new(),
76                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
77                }
78            }
79        }
80    }
81
82    /// Generate a primitive type struct with special FHIR primitive type semantics
83    pub fn generate_primitive_type_struct(
84        &mut self,
85        structure_def: &StructureDefinition,
86        mut rust_struct: RustStruct,
87    ) -> CodegenResult<RustStruct> {
88        // For primitive types, don't inherit from Element
89        rust_struct.base_definition = None;
90
91        // Map FHIR primitive types to Rust types
92        let rust_primitive_type = Self::map_fhir_primitive_to_rust_type(&structure_def.name);
93
94        // The primitive type is just a type alias or newtype wrapper around the Rust primitive
95        // For now, we'll create a struct with a single `value` field
96        let value_field = RustField::new("value".to_string(), rust_primitive_type);
97        rust_struct.add_field(value_field);
98
99        // Cache the generated struct for future use
100        let struct_name = rust_struct.name.clone();
101        self.type_cache.insert(struct_name, rust_struct.clone());
102
103        Ok(rust_struct)
104    }
105
106    /// Generate the companion Element struct for a primitive type
107    pub fn generate_primitive_element_struct(
108        &mut self,
109        structure_def: &StructureDefinition,
110    ) -> CodegenResult<RustStruct> {
111        // Generate the Element struct name (e.g., _uri for uri primitive)
112        let element_struct_name = format!("_{}", structure_def.name);
113
114        // Check if we've already generated this Element struct
115        if let Some(cached_struct) = self.type_cache.get(&element_struct_name) {
116            return Ok(cached_struct.clone());
117        }
118
119        // Create the Element struct
120        let mut element_struct = RustStruct::new(element_struct_name.clone());
121
122        // Add documentation
123        element_struct.doc_comment = Some(
124            DocumentationGenerator::generate_primitive_element_documentation(&structure_def.name),
125        );
126
127        // Use config to determine derives
128        let mut derives = vec!["Debug".to_string(), "Clone".to_string()];
129        if self.config.with_serde {
130            derives.extend(vec!["Serialize".to_string(), "Deserialize".to_string()]);
131        }
132        element_struct.derives = derives;
133
134        // Set base as Element since these extend Element
135        element_struct.base_definition = Some("Element".to_string());
136
137        // Cache the generated Element struct for future use
138        self.type_cache
139            .insert(element_struct_name, element_struct.clone());
140
141        Ok(element_struct)
142    }
143
144    /// Map FHIR primitive type names to Rust types
145    pub fn map_fhir_primitive_to_rust_type(primitive_name: &str) -> RustType {
146        match primitive_name {
147            "boolean" => RustType::Boolean,
148            "integer" | "positiveInt" | "unsignedInt" => RustType::Integer,
149            "decimal" => RustType::Float,
150            "string" | "code" | "id" | "markdown" | "uri" | "url" | "canonical" | "oid"
151            | "uuid" | "base64Binary" | "xhtml" => RustType::String,
152            "date" | "dateTime" | "time" | "instant" => RustType::String, // Could use chrono types later
153            _ => RustType::String, // Default to String for unknown primitive types
154        }
155    }
156
157    /// Check if a given name represents a FHIR primitive type
158    pub fn is_fhir_primitive_type(name: &str) -> bool {
159        matches!(
160            name,
161            "boolean"
162                | "integer"
163                | "positiveInt"
164                | "unsignedInt"
165                | "decimal"
166                | "string"
167                | "code"
168                | "id"
169                | "markdown"
170                | "uri"
171                | "url"
172                | "canonical"
173                | "oid"
174                | "uuid"
175                | "base64Binary"
176                | "xhtml"
177                | "date"
178                | "dateTime"
179                | "time"
180                | "instant"
181        )
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_map_fhir_primitive_to_rust_type() {
191        assert!(matches!(
192            PrimitiveGenerator::map_fhir_primitive_to_rust_type("boolean"),
193            RustType::Boolean
194        ));
195        assert!(matches!(
196            PrimitiveGenerator::map_fhir_primitive_to_rust_type("integer"),
197            RustType::Integer
198        ));
199        assert!(matches!(
200            PrimitiveGenerator::map_fhir_primitive_to_rust_type("decimal"),
201            RustType::Float
202        ));
203        assert!(matches!(
204            PrimitiveGenerator::map_fhir_primitive_to_rust_type("string"),
205            RustType::String
206        ));
207        assert!(matches!(
208            PrimitiveGenerator::map_fhir_primitive_to_rust_type("uri"),
209            RustType::String
210        ));
211        assert!(matches!(
212            PrimitiveGenerator::map_fhir_primitive_to_rust_type("unknown"),
213            RustType::String
214        ));
215    }
216
217    #[test]
218    fn test_is_fhir_primitive_type() {
219        assert!(PrimitiveGenerator::is_fhir_primitive_type("boolean"));
220        assert!(PrimitiveGenerator::is_fhir_primitive_type("string"));
221        assert!(PrimitiveGenerator::is_fhir_primitive_type("integer"));
222        assert!(PrimitiveGenerator::is_fhir_primitive_type("decimal"));
223        assert!(PrimitiveGenerator::is_fhir_primitive_type("uri"));
224        assert!(PrimitiveGenerator::is_fhir_primitive_type("dateTime"));
225
226        assert!(!PrimitiveGenerator::is_fhir_primitive_type("Patient"));
227        assert!(!PrimitiveGenerator::is_fhir_primitive_type("Identifier"));
228        assert!(!PrimitiveGenerator::is_fhir_primitive_type("unknown"));
229    }
230
231    #[test]
232    fn test_generate_primitive_type_alias() {
233        use crate::config::CodegenConfig;
234
235        let config = CodegenConfig::default();
236        let mut type_cache = HashMap::new();
237        let primitive_generator = PrimitiveGenerator::new(&config, &mut type_cache);
238
239        let structure_def = StructureDefinition {
240            resource_type: "StructureDefinition".to_string(),
241            id: "boolean".to_string(),
242            url: "http://hl7.org/fhir/StructureDefinition/boolean".to_string(),
243            name: "boolean".to_string(),
244            title: Some("boolean".to_string()),
245            status: "active".to_string(),
246            kind: "primitive-type".to_string(),
247            is_abstract: false,
248            description: Some("Value of 'true' or 'false'".to_string()),
249            purpose: None,
250            base_type: "boolean".to_string(),
251            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Element".to_string()),
252            version: None,
253            differential: None,
254            snapshot: None,
255        };
256
257        let result = primitive_generator.generate_primitive_type_alias(&structure_def);
258        assert!(result.is_ok());
259
260        let type_alias = result.unwrap();
261        assert_eq!(type_alias.name, "BooleanType");
262        assert!(matches!(type_alias.target_type, RustType::Boolean));
263    }
264
265    #[test]
266    fn test_primitive_type_naming_convention() {
267        use crate::config::CodegenConfig;
268
269        let config = CodegenConfig::default();
270        let mut type_cache = HashMap::new();
271        let primitive_generator = PrimitiveGenerator::new(&config, &mut type_cache);
272
273        // Test various primitive type naming conventions
274        let test_cases = vec![
275            ("string", "StringType"),
276            ("integer", "IntegerType"),
277            ("boolean", "BooleanType"),
278            ("decimal", "DecimalType"),
279            ("uri", "UriType"),
280            ("dateTime", "DateTimeType"),
281            ("time", "TimeType"),
282            ("instant", "InstantType"),
283            ("positiveInt", "PositiveIntType"),
284        ];
285
286        for (input_name, expected_name) in test_cases {
287            let structure_def = StructureDefinition {
288                resource_type: "StructureDefinition".to_string(),
289                id: input_name.to_string(),
290                url: format!("http://hl7.org/fhir/StructureDefinition/{input_name}"),
291                name: input_name.to_string(),
292                title: Some(input_name.to_string()),
293                status: "active".to_string(),
294                kind: "primitive-type".to_string(),
295                is_abstract: false,
296                description: Some(format!("FHIR {input_name} primitive type")),
297                purpose: None,
298                base_type: input_name.to_string(),
299                base_definition: Some(
300                    "http://hl7.org/fhir/StructureDefinition/Element".to_string(),
301                ),
302                version: None,
303                differential: None,
304                snapshot: None,
305            };
306
307            let result = primitive_generator.generate_primitive_type_alias(&structure_def);
308            assert!(
309                result.is_ok(),
310                "Failed to generate type alias for {input_name}"
311            );
312
313            let type_alias = result.unwrap();
314            assert_eq!(
315                type_alias.name, expected_name,
316                "Expected {expected_name} for {input_name}, got {}",
317                type_alias.name
318            );
319        }
320    }
321
322    #[test]
323    fn test_generate_primitive_element_struct() {
324        use crate::config::CodegenConfig;
325
326        let config = CodegenConfig::default();
327        let mut type_cache = HashMap::new();
328        let mut primitive_generator = PrimitiveGenerator::new(&config, &mut type_cache);
329
330        let structure_def = StructureDefinition {
331            resource_type: "StructureDefinition".to_string(),
332            id: "string".to_string(),
333            url: "http://hl7.org/fhir/StructureDefinition/string".to_string(),
334            name: "string".to_string(),
335            title: Some("string".to_string()),
336            status: "active".to_string(),
337            kind: "primitive-type".to_string(),
338            is_abstract: false,
339            description: Some("A sequence of Unicode characters".to_string()),
340            purpose: None,
341            base_type: "string".to_string(),
342            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Element".to_string()),
343            version: None,
344            differential: None,
345            snapshot: None,
346        };
347
348        let result = primitive_generator.generate_primitive_element_struct(&structure_def);
349        assert!(result.is_ok());
350
351        let element_struct = result.unwrap();
352        assert_eq!(element_struct.name, "_string");
353        assert_eq!(element_struct.base_definition, Some("Element".to_string()));
354        assert!(element_struct.derives.contains(&"Debug".to_string()));
355        assert!(element_struct.derives.contains(&"Clone".to_string()));
356    }
357}