Skip to main content

rh_codegen/generators/
crate_generator.rs

1//! FHIR Crate Generation
2//!
3//! This module provides functionality to generate complete Rust crates from FHIR packages,
4//! including Cargo.toml, lib.rs, README.md, and proper module structure.
5
6use std::fs;
7use std::path::Path;
8
9use anyhow::Result;
10use chrono::Local;
11
12/// Parameters for crate generation
13#[derive(Debug, Clone)]
14pub struct CrateGenerationParams<'a> {
15    /// Output directory for the crate
16    pub output: &'a Path,
17    /// Package name (e.g., "hl7.fhir.r4.core")
18    pub package: &'a str,
19    /// Package version (e.g., "4.0.1")
20    pub version: &'a str,
21    /// Canonical URL from package.json
22    pub canonical_url: &'a str,
23    /// Author from package.json
24    pub author: &'a str,
25    /// Description from package.json
26    pub description: &'a str,
27    /// Command that was invoked to generate this crate
28    pub command_invoked: &'a str,
29    /// Optional override for the generated crate name (e.g., "rh-hl7-fhir-r4-core").
30    /// When None, the name is auto-derived from the FHIR package name.
31    pub crate_name: Option<&'a str>,
32}
33
34/// Statistics about generated crate content
35#[derive(Debug, Clone)]
36pub struct CrateStatistics {
37    /// Number of generated structs
38    pub num_structs: usize,
39    /// Number of generated enums
40    pub num_enums: usize,
41    /// Total number of types
42    pub total_types: usize,
43    /// Canonical URL
44    pub canonical_url: String,
45}
46
47/// Generate a complete Rust crate structure with idiomatic directory organization
48pub fn generate_crate_structure(params: CrateGenerationParams) -> Result<()> {
49    // Create the src directory structure
50    let src_dir = params.output.join("src");
51    fs::create_dir_all(&src_dir)?;
52
53    // Create subdirectories
54    let resource_dir = src_dir.join("resources");
55    let datatypes_dir = src_dir.join("datatypes");
56    let extensions_dir = src_dir.join("extensions");
57    let primitives_dir = src_dir.join("primitives");
58    let traits_dir = src_dir.join("traits");
59    let bindings_dir = src_dir.join("bindings");
60    let profiles_dir = src_dir.join("profiles");
61
62    fs::create_dir_all(&resource_dir)?;
63    fs::create_dir_all(&datatypes_dir)?;
64    fs::create_dir_all(&extensions_dir)?;
65    fs::create_dir_all(&primitives_dir)?;
66    fs::create_dir_all(&traits_dir)?;
67    fs::create_dir_all(&bindings_dir)?;
68    fs::create_dir_all(&profiles_dir)?;
69
70    // Generate statistics by counting organized files (if any exist from organized generation)
71    let stats = generate_crate_statistics_from_organized_dirs(
72        &resource_dir,
73        &datatypes_dir,
74        &extensions_dir,
75        &primitives_dir,
76    )?;
77
78    // Generate Cargo.toml
79    let cargo_toml_content = generate_cargo_toml(
80        params.package,
81        params.version,
82        params.output,
83        params.crate_name,
84    );
85    let cargo_toml_path = params.output.join("Cargo.toml");
86    fs::write(&cargo_toml_path, cargo_toml_content)?;
87
88    // Generate lib.rs with new structure
89    let lib_rs_content = generate_lib_rs_idiomatic();
90    let lib_rs_path = src_dir.join("lib.rs");
91    fs::write(&lib_rs_path, lib_rs_content)?;
92
93    // Generate macros.rs with FHIR primitive macros
94    let macros_content = include_str!("../macros.rs");
95    let macros_path = src_dir.join("macros.rs");
96    fs::write(&macros_path, macros_content)?;
97
98    // Generate validation.rs module with ValidatableResource trait
99    let validation_content =
100        crate::generators::ValidationTraitGenerator::generate_validation_module();
101    let validation_path = src_dir.join("validation.rs");
102    fs::write(&validation_path, validation_content)?;
103
104    // Generate prelude.rs module with commonly used trait re-exports
105    let prelude_content = generate_prelude_module();
106    let prelude_path = src_dir.join("prelude.rs");
107    fs::write(&prelude_path, prelude_content)?;
108
109    // Generate mod.rs files for each module
110    generate_module_files(
111        &resource_dir,
112        &datatypes_dir,
113        &extensions_dir,
114        &primitives_dir,
115        &traits_dir,
116        &bindings_dir,
117        &profiles_dir,
118    )?;
119
120    // Generate README.md
121    let readme_content = generate_readme_md(
122        params.package,
123        params.version,
124        params.canonical_url,
125        params.author,
126        params.description,
127        params.command_invoked,
128        &stats,
129        params.crate_name,
130    );
131    let readme_path = params.output.join("README.md");
132    fs::write(&readme_path, readme_content)?;
133
134    // Run quality checks as a final step
135    //let quality_config = QualityConfig::default();
136    //run_quality_checks(params.output, &quality_config)
137    //   .map_err(|e| anyhow::anyhow!("Quality checks failed: {e}"))?;
138
139    Ok(())
140}
141
142/// Generate Cargo.toml content for the FHIR crate
143fn generate_cargo_toml(
144    package: &str,
145    version: &str,
146    output_dir: &Path,
147    crate_name_override: Option<&str>,
148) -> String {
149    // Derive the package name: use override if provided, else convert FHIR package name
150    let derived = package.replace(['.', '-'], "_");
151    let crate_name = crate_name_override.unwrap_or(&derived);
152    // Lib name must be a valid Rust identifier (no hyphens)
153    let lib_name = crate_name.replace('-', "_");
154
155    let rh_foundation_path = foundation_relative_path(output_dir);
156    let rh_foundation_version = env!("CARGO_PKG_VERSION");
157
158    // Derive a release-keyword (e.g. "fhir-r4") from package segments like "r4"/"r5"
159    let release_keyword = package
160        .split('.')
161        .find(|seg| {
162            seg.len() >= 2 && seg.starts_with('r') && seg[1..].chars().all(|c| c.is_ascii_digit())
163        })
164        .map(|seg| format!("\"fhir-{seg}\", "))
165        .unwrap_or_default();
166
167    format!(
168        r#"[package]
169name = "{crate_name}"
170version.workspace = true
171edition.workspace = true
172license.workspace = true
173repository.workspace = true
174homepage.workspace = true
175rust-version.workspace = true
176description = "Generated FHIR types from {package} package version {version}"
177authors.workspace = true
178keywords = ["fhir", {release_keyword}"healthcare", "hl7", "types"]
179categories = ["api-bindings", "data-structures"]
180readme = "README.md"
181
182[dependencies]
183serde = {{ version = "1.0", features = ["derive"] }}
184serde_json = "1.0"
185phf = {{ version = "0.11", features = ["macros"] }}
186once_cell = "1.19"
187rh-foundation = {{ path = "{rh_foundation_path}", version = "{rh_foundation_version}" }}
188
189[lib]
190name = "{lib_name}"
191path = "src/lib.rs"
192
193[lints]
194workspace = true
195"#
196    )
197}
198
199/// Compute the path to `rh-foundation` relative to the generated crate's directory.
200///
201/// Generated crates conventionally live as siblings of `rh-foundation` under
202/// `crates/`, so the default is `../rh-foundation`. When the output directory
203/// resolves to somewhere else inside this workspace, a correct relative path is
204/// computed instead. Absolute paths are never emitted (see refactor plan C1).
205fn foundation_relative_path(output_dir: &Path) -> String {
206    let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else {
207        return "../rh-foundation".to_string();
208    };
209    let Some(workspace_root) = Path::new(&manifest_dir).parent().and_then(|p| p.parent()) else {
210        return "../rh-foundation".to_string();
211    };
212    let foundation = workspace_root.join("crates/rh-foundation");
213    let (Ok(foundation), Ok(output)) = (foundation.canonicalize(), output_dir.canonicalize())
214    else {
215        return "../rh-foundation".to_string();
216    };
217
218    let from: Vec<_> = output.components().collect();
219    let to: Vec<_> = foundation.components().collect();
220    let common = from
221        .iter()
222        .zip(to.iter())
223        .take_while(|(a, b)| a == b)
224        .count();
225    let mut parts: Vec<String> = vec!["..".to_string(); from.len() - common];
226    parts.extend(
227        to[common..]
228            .iter()
229            .map(|c| c.as_os_str().to_string_lossy().into_owned()),
230    );
231    if parts.is_empty() {
232        ".".to_string()
233    } else {
234        parts.join("/")
235    }
236}
237
238/// Generate lib.rs content with idiomatic module structure
239fn generate_lib_rs_idiomatic() -> String {
240    let lib_content = r#"//! Generated FHIR Rust bindings
241//!
242//! This crate contains Rust types and traits for FHIR resources and data types.
243//! It includes macros for primitive field generation and maintains FHIR compliance.
244
245// Allow clippy lint for derivable Default implementations
246//
247// TODO: Future optimization - derive Default when possible instead of manual impl
248//
249// Currently, we generate explicit Default implementations for all structs.
250// Many of these could use #[derive(Default)] instead, which would be more idiomatic.
251//
252// Pros of deriving Default:
253// - More idiomatic Rust code
254// - Less generated code (no manual impl blocks)
255// - Clearer intent (all fields use Default::default())
256//
257// Cons of current approach (manual impl):
258// - Clippy warns about 1,100+ derivable implementations
259// - More verbose generated code
260//
261// Pros of current approach:
262// - Explicit and predictable behavior
263// - Handles mixed initialization patterns consistently
264// - Simpler code generation logic
265//
266// To implement derive-based approach would require:
267// 1. Analyze all field types to ensure they implement Default
268// 2. Detect required fields with non-Default initializations (String::new(), Vec::new(), etc.)
269// 3. Add "Default" to struct derives only when ALL fields can use Default::default()
270// 4. Skip manual impl generation for those structs
271//
272#![allow(clippy::derivable_impls)]
273
274pub mod macros;
275pub mod metadata;
276pub mod primitives;
277pub mod datatypes;
278pub mod extensions;
279pub mod resources;
280pub mod profiles;
281pub mod traits;
282pub mod bindings;
283pub mod validation;
284pub mod prelude;
285
286pub use serde::{Deserialize, Serialize};
287"#;
288
289    lib_content.to_string()
290}
291/// Generate mod.rs files for each module directory
292pub fn generate_module_files(
293    resource_dir: &Path,
294    datatypes_dir: &Path,
295    extensions_dir: &Path,
296    primitives_dir: &Path,
297    traits_dir: &Path,
298    bindings_dir: &Path,
299    profiles_dir: &Path,
300) -> Result<()> {
301    // Generate resource/mod.rs
302    let resource_mod_content = generate_mod_rs_for_directory(resource_dir, "FHIR resource types")?;
303    fs::write(resource_dir.join("mod.rs"), resource_mod_content)?;
304
305    // Generate datatypes/mod.rs
306    let datatypes_mod_content = generate_mod_rs_for_directory(datatypes_dir, "FHIR data types")?;
307    fs::write(datatypes_dir.join("mod.rs"), datatypes_mod_content)?;
308
309    // Generate extensions/mod.rs
310    let extensions_mod_content =
311        generate_mod_rs_for_directory(extensions_dir, "FHIR extension types")?;
312    fs::write(extensions_dir.join("mod.rs"), extensions_mod_content)?;
313
314    // Generate primitives/mod.rs
315    let primitives_mod_content =
316        generate_mod_rs_for_directory(primitives_dir, "FHIR primitive types")?;
317    fs::write(primitives_dir.join("mod.rs"), primitives_mod_content)?;
318
319    // Generate traits/mod.rs
320    let traits_mod_content = generate_mod_rs_for_directory(
321        traits_dir,
322        "FHIR trait definitions for resources and profiles",
323    )?;
324    fs::write(traits_dir.join("mod.rs"), traits_mod_content)?;
325
326    // Generate bindings/mod.rs for ValueSet enums
327    let bindings_mod_content =
328        generate_mod_rs_for_directory(bindings_dir, "FHIR ValueSet bindings and enums")?;
329    fs::write(bindings_dir.join("mod.rs"), bindings_mod_content)?;
330
331    // Generate profiles/mod.rs for FHIR profiles
332    let profiles_mod_content =
333        generate_mod_rs_for_directory(profiles_dir, "FHIR profiles derived from core resources")?;
334    fs::write(profiles_dir.join("mod.rs"), profiles_mod_content)?;
335
336    Ok(())
337}
338
339/// Generate mod.rs content for a specific directory
340fn generate_mod_rs_for_directory(dir: &Path, description: &str) -> Result<String> {
341    let mut content = String::new();
342    content.push_str(&format!("//! {description}\n\n"));
343
344    // Get all .rs files in the directory
345    let mut rs_files = Vec::new();
346    if dir.exists() {
347        for entry in fs::read_dir(dir)? {
348            let entry = entry?;
349            let path = entry.path();
350            if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
351                if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
352                    if stem != "mod" {
353                        rs_files.push(stem.to_string());
354                    }
355                }
356            }
357        }
358    }
359
360    // Sort module names for consistency
361    rs_files.sort();
362
363    // Generate module declarations
364    for module_name in &rs_files {
365        content.push_str(&format!("pub mod {module_name};\n"));
366    }
367
368    // Note: No glob re-exports to avoid ambiguous re-export warnings
369    // Individual types can be imported explicitly when needed
370
371    Ok(content)
372}
373
374/// Generate statistics from organized directories
375fn generate_crate_statistics_from_organized_dirs(
376    resource_dir: &Path,
377    datatypes_dir: &Path,
378    extensions_dir: &Path,
379    primitives_dir: &Path,
380) -> Result<CrateStatistics> {
381    let mut num_structs = 0;
382
383    // Count files in each directory
384    for dir in [resource_dir, datatypes_dir, extensions_dir, primitives_dir] {
385        if dir.exists() {
386            for entry in fs::read_dir(dir)? {
387                let entry = entry?;
388                let path = entry.path();
389                if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
390                    if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
391                        if stem != "mod" {
392                            num_structs += 1;
393
394                            // Count additional structs within the file (nested types)
395                            if let Ok(content) = fs::read_to_string(&path) {
396                                num_structs +=
397                                    content.matches("pub struct ").count().saturating_sub(1);
398                            }
399                        }
400                    }
401                }
402            }
403        }
404    }
405
406    Ok(CrateStatistics {
407        num_structs,
408        num_enums: 0,
409        total_types: num_structs,
410        canonical_url: "Unknown".to_string(),
411    })
412}
413
414/// Generate README.md content with package information and statistics
415#[allow(clippy::too_many_arguments)]
416fn generate_readme_md(
417    package: &str,
418    version: &str,
419    canonical_url: &str,
420    author: &str,
421    description: &str,
422    command_invoked: &str,
423    stats: &CrateStatistics,
424    crate_name_override: Option<&str>,
425) -> String {
426    let derived = package.replace(['.', '-'], "_");
427    // Use lib name form (underscores) for Rust `use` statements in README
428    let crate_name = crate_name_override.unwrap_or(&derived).replace('-', "_");
429    let mut content = String::new();
430
431    content.push_str(&format!("# {crate_name}\n\n"));
432    content.push_str(&format!("**Generated FHIR Types for {package}**\n\n"));
433    content.push_str(&format!("This crate contains automatically generated Rust types for FHIR (Fast Healthcare Interoperability Resources) based on the `{package}` package.\n\n"));
434
435    content.push_str("## Important Notice\n\n");
436    content
437        .push_str("**This crate was automatically generated using the RH codegen CLI tool.**\n\n");
438    content.push_str(&format!(
439        "- **Generator command**:\n```bash\n{command_invoked}\n```\n\n"
440    ));
441    content.push_str(&format!("- **Generation timestamp**: {}\n\n", Local::now()));
442
443    content.push_str("## Package Information\n\n");
444
445    content.push_str(&format!("* **Package Name** {package}\n"));
446    content.push_str(&format!("* **Package Author** {author}\n"));
447    content.push_str(&format!("* **Version** {version}\n"));
448    content.push_str(&format!("* **Canonical URL** `{canonical_url}`\n\n"));
449
450    content.push_str(&format!(
451        "**Statistics: {} structs, {} enums, {} total types**\n\n",
452        stats.num_structs, stats.num_enums, stats.total_types
453    ));
454
455    content.push_str(&format!("## Description\n\n{description}"));
456
457    content.push_str("\n\n");
458
459    content.push_str("## Features\n\n");
460    content.push_str(
461        "- **Complete FHIR type definitions** - All resources, datatypes, and primitives\n",
462    );
463    content.push_str(
464        "- **Serde serialization** - Built-in JSON serialization/deserialization support\n",
465    );
466    content.push_str(
467        "- **Type metadata** - Compile-time metadata for field types and path resolution\n",
468    );
469    content.push_str(
470        "- **Idiomatic Rust** - Clean, organized module structure with proper naming conventions\n",
471    );
472    content.push_str("- **Zero-cost abstractions** - PHF (perfect hash function) maps for O(1) metadata lookups\n\n");
473
474    content.push_str("## Usage\n\n");
475    content.push_str("Add this crate to your `Cargo.toml`:\n\n");
476    content.push_str("```toml\n");
477    content.push_str("[dependencies]\n");
478    content.push_str(&format!("{crate_name} = \"0.1.0\"\n"));
479    content.push_str("```\n\n");
480
481    content.push_str("### Deserializing FHIR Resources\n\n");
482    content.push_str("```rust\n");
483    content.push_str(&format!("use {crate_name}::resources::patient::Patient;\n"));
484    content.push_str("use serde_json;\n\n");
485    content.push_str("let json_data = r#\"{\\\"resourceType\\\": \\\"Patient\\\", \\\"id\\\": \\\"example\\\"}\"#;\n");
486    content.push_str("let patient: Patient = serde_json::from_str(json_data)?;\n\n");
487    content.push_str("println!(\"Patient ID: {}\", patient.id.unwrap_or_default());\n");
488    content.push_str("```\n\n");
489
490    content.push_str("### Creating Resources Programmatically\n\n");
491    content.push_str("This crate provides two idiomatic ways to work with FHIR resources using builder traits:\n\n");
492
493    content.push_str("#### Option 1: Resource Module with Re-exported Traits (Recommended)\n\n");
494    content.push_str("Each resource module re-exports its associated traits for convenience:\n\n");
495    content.push_str("```rust\n");
496    content.push_str("// Import resource with its traits - all in one place!\n");
497    content.push_str(&format!(
498        "use {crate_name}::resources::patient::{{Patient, PatientMutators}};\n"
499    ));
500    content.push_str(&format!(
501        "use {crate_name}::prelude::*;  // Gets base traits (ResourceMutators, etc.)\n"
502    ));
503    content.push_str(&format!(
504        "use {crate_name}::datatypes::human_name::HumanName;\n\n"
505    ));
506    content.push_str("// Build a patient using the builder pattern\n");
507    content.push_str("let patient = <Patient as PatientMutators>::new()\n");
508    content.push_str("    .set_id(\"patient-123\".to_string())\n");
509    content.push_str("    .set_active(true)\n");
510    content.push_str("    .add_name(HumanName {\n");
511    content.push_str("        family: Some(\"Doe\".to_string()),\n");
512    content.push_str("        given: vec![\"John\".to_string()],\n");
513    content.push_str("        ..Default::default()\n");
514    content.push_str("    })\n");
515    content.push_str("    .set_gender(Some(AdministrativeGender::Male))\n");
516    content.push_str("    .set_birth_date(\"1990-01-15\".to_string());\n");
517    content.push_str("```\n\n");
518
519    content.push_str("#### Option 2: Prelude Module\n\n");
520    content.push_str("For common base traits, use the prelude module:\n\n");
521    content.push_str("```rust\n");
522    content.push_str(&format!(
523        "use {crate_name}::prelude::*;  // ValidatableResource, ResourceMutators, etc.\n"
524    ));
525    content.push_str(&format!(
526        "use {crate_name}::resources::patient::{{Patient, PatientMutators}};\n\n"
527    ));
528    content.push_str("let patient = <Patient as PatientMutators>::new()\n");
529    content.push_str("    .set_id(\"example\".to_string());\n");
530    content.push_str("```\n\n");
531    content.push_str("The prelude includes:\n");
532    content.push_str("- `ValidatableResource` - Access invariants and validation rules\n");
533    content.push_str("- `ResourceMutators` - Builder methods for all resources\n");
534    content.push_str("- `DomainResourceMutators` - Builder methods for domain resources\n\n");
535
536    content.push_str("#### Direct Struct Construction\n\n");
537    content.push_str("You can also construct resources directly:\n\n");
538    content.push_str("```rust\n");
539    content.push_str(&format!("use {crate_name}::resources::patient::Patient;\n"));
540    content.push_str(&format!(
541        "use {crate_name}::datatypes::human_name::HumanName;\n"
542    ));
543    content.push_str(&format!(
544        "use {crate_name}::bindings::administrative_gender::AdministrativeGender;\n\n"
545    ));
546    content.push_str("let patient = Patient {\n");
547    content.push_str("    id: Some(\"patient-123\".to_string()),\n");
548    content.push_str("    active: Some(true),\n");
549    content.push_str("    name: vec![HumanName {\n");
550    content.push_str("        family: Some(\"Doe\".to_string()),\n");
551    content.push_str("        given: vec![\"John\".to_string()],\n");
552    content.push_str("        ..Default::default()\n");
553    content.push_str("    }],\n");
554    content.push_str("    gender: Some(AdministrativeGender::Male),\n");
555    content.push_str("    birth_date: Some(\"1990-01-15\".to_string()),\n");
556    content.push_str("    ..Default::default()\n");
557    content.push_str("};\n");
558    content.push_str("```\n\n");
559
560    content.push_str("### Using Type Metadata\n\n");
561    content.push_str("This crate includes compile-time metadata for all FHIR types, enabling runtime type introspection and path resolution:\n\n");
562    content.push_str("```rust\n");
563    content.push_str(&format!("use {crate_name}::metadata::{{resolve_path, get_field_info, FhirFieldType, FhirPrimitiveType}};\n\n"));
564    content.push_str("// Resolve nested paths to their FHIR types\n");
565    content.push_str("if let Some(field_type) = resolve_path(\"Patient.birthDate\") {\n");
566    content.push_str("    match field_type {\n");
567    content.push_str("        FhirFieldType::Primitive(FhirPrimitiveType::Date) => {\n");
568    content.push_str("            println!(\"birthDate is a FHIR date type\");\n");
569    content.push_str("        }\n");
570    content.push_str("        _ => {}\n");
571    content.push_str("    }\n");
572    content.push_str("}\n\n");
573    content.push_str("// Resolve complex nested paths\n");
574    content.push_str("if let Some(field_type) = resolve_path(\"Patient.name.given\") {\n");
575    content.push_str("    match field_type {\n");
576    content.push_str("        FhirFieldType::Primitive(FhirPrimitiveType::String) => {\n");
577    content.push_str("            println!(\"name.given is a string array\");\n");
578    content.push_str("        }\n");
579    content.push_str("        _ => {}\n");
580    content.push_str("    }\n");
581    content.push_str("}\n\n");
582    content.push_str("// Get field information directly\n");
583    content.push_str("if let Some(field_info) = get_field_info(\"Patient\", \"active\") {\n");
584    content.push_str("    println!(\"Min cardinality: {}\", field_info.min);\n");
585    content.push_str("    println!(\"Max cardinality: {:?}\", field_info.max);\n");
586    content.push_str("    println!(\"Is choice type: {}\", field_info.is_choice_type);\n");
587    content.push_str("}\n");
588    content.push_str("```\n\n");
589
590    content.push_str("The metadata system enables:\n");
591    content.push_str("- **Path resolution** - Navigate nested paths like `Patient.name.given`\n");
592    content.push_str("- **Type introspection** - Determine field types at runtime\n");
593    content.push_str("- **Cardinality information** - Min/max occurrence constraints\n");
594    content.push_str("- **Choice type detection** - Identify polymorphic fields\n");
595    content
596        .push_str("- **Zero runtime cost** - All lookups use compile-time perfect hash maps\n\n");
597
598    content.push_str("## Structure\n\n");
599    content.push_str("This crate organizes FHIR types into logical modules:\n\n");
600    content.push_str("- **resources/** - All FHIR resources (Patient, Observation, etc.)\n");
601    content.push_str("- **profiles/** - FHIR profiles (Vitalsigns, BodyHeight, etc.)\n");
602    content.push_str(
603        "- **datatypes/** - Complex and primitive datatypes (HumanName, Address, etc.)\n",
604    );
605    content.push_str("- **bindings/** - ValueSet enumerations (AdministrativeGender, etc.)\n");
606    content.push_str("- **primitives/** - Base primitive types (DateType, DateTimeType, etc.)\n");
607    content.push_str("- **traits/** - Mutator, accessor, and existence traits for all types\n");
608    content.push_str(
609        "- **prelude.rs** - Commonly used traits (ValidatableResource, ResourceMutators, etc.)\n",
610    );
611    content.push_str("- **metadata/** - Type metadata split by category (resources, datatypes, primitives) for faster incremental compilation\n\n");
612
613    content.push_str("## Regenerating This Crate\n\n");
614    content.push_str("To regenerate this crate with updated FHIR definitions:\n\n");
615    content.push_str("```bash\n");
616    content.push_str(command_invoked);
617    content.push_str("\n```\n\n");
618
619    content.push_str("## License\n\n");
620    content.push_str("This generated crate is provided under MIT OR Apache-2.0 license.\n\n");
621
622    content.push_str("## Related Links\n\n");
623    content.push_str("- [FHIR Specification](https://hl7.org/fhir/)\n");
624    content.push_str("- [FHIR Package Registry](https://packages.fhir.org/)\n");
625    content.push_str("- [RH Project](https://github.com/reasonhealth/rh)\n\n");
626    content.push_str("---\n\n");
627    content.push_str(&format!(
628        "*Generated by RH codegen tool at {}*\n",
629        Local::now()
630    ));
631
632    content
633}
634
635/// Generate a prelude module with commonly used trait re-exports
636fn generate_prelude_module() -> String {
637    r#"//! Prelude module - commonly used traits for convenience
638//!
639//! This module re-exports the most commonly used traits for working with
640//! FHIR resources. Import this module to avoid having to import individual
641//! traits from the `traits` module.
642//!
643//! # Example
644//!
645//! ```ignore
646//! use hl7_fhir_r4_core::prelude::*;
647//! use hl7_fhir_r4_core::resources::patient::Patient;
648//!
649//! // All mutator traits are now in scope
650//! let patient = <Patient as PatientMutators>::new()
651//!     .set_id("example".to_string())
652//!     .set_active(true);
653//! ```
654
655// Resource mutator traits - for building resources with method chaining
656pub use crate::traits::resource::ResourceMutators;
657pub use crate::traits::domain_resource::DomainResourceMutators;
658
659// Note: Individual resource mutator traits (PatientMutators, ObservationMutators, etc.)
660// are re-exported from their respective resource modules for convenience.
661// For example: use hl7_fhir_r4_core::resources::patient::PatientMutators;
662
663// Validation trait
664pub use crate::validation::ValidatableResource;
665"#
666    .to_string()
667}
668
669/// Parse package.json to extract metadata for crate generation
670pub fn parse_package_metadata(package_json_path: &Path) -> Result<(String, String, String)> {
671    let package_json_content = fs::read_to_string(package_json_path)?;
672    let package_json: serde_json::Value = serde_json::from_str(&package_json_content)?;
673
674    let canonical = package_json
675        .get("canonical")
676        .and_then(|v| v.as_str())
677        .unwrap_or("Unknown")
678        .to_string();
679
680    let author = package_json
681        .get("author")
682        .and_then(|v| v.as_str())
683        .unwrap_or("FHIR Code Generator")
684        .to_string();
685
686    let description = package_json
687        .get("description")
688        .and_then(|v| v.as_str())
689        .unwrap_or("Generated FHIR types crate.")
690        .to_string();
691
692    Ok((canonical, author, description))
693}
694
695impl<'a> crate::generators::file_generator::FileGenerator<'a> {
696    pub fn generate_complete_crate<P: AsRef<Path>>(
697        &self,
698        output_dir: P,
699        crate_name: &str,
700        _structures: &[crate::fhir_types::StructureDefinition],
701    ) -> crate::CodegenResult<()> {
702        let output_dir = output_dir.as_ref();
703
704        let src_dir = output_dir.join("src");
705        fs::create_dir_all(&src_dir)?;
706
707        let primitives_dir = src_dir.join("primitives");
708        let datatypes_dir = src_dir.join("datatypes");
709        let extensions_dir = src_dir.join("extensions");
710        let resource_dir = src_dir.join("resource");
711        let traits_dir = src_dir.join("traits");
712
713        fs::create_dir_all(&primitives_dir)?;
714        fs::create_dir_all(&datatypes_dir)?;
715        fs::create_dir_all(&extensions_dir)?;
716        fs::create_dir_all(&resource_dir)?;
717        fs::create_dir_all(&traits_dir)?;
718
719        self.generate_lib_file(src_dir.join("lib.rs"))?;
720        self.generate_macros_file(src_dir.join("macros.rs"))?;
721        self.generate_combined_primitives_file(&[], primitives_dir.join("mod.rs"))?;
722
723        let cargo_toml_path = output_dir.join("Cargo.toml");
724        if !cargo_toml_path.exists() {
725            self.generate_cargo_toml(&cargo_toml_path, crate_name)?;
726        }
727
728        self.generate_module_file(&datatypes_dir, &[])?;
729        self.generate_module_file(&extensions_dir, &[])?;
730        self.generate_module_file(&resource_dir, &[])?;
731        self.generate_module_file(&traits_dir, &[])?;
732
733        Ok(())
734    }
735
736    pub(crate) fn generate_cargo_toml<P: AsRef<Path>>(
737        &self,
738        cargo_path: P,
739        crate_name: &str,
740    ) -> crate::CodegenResult<()> {
741        let cargo_content = format!(
742            r#"[package]
743name = "{crate_name}"
744version = "0.1.0"
745edition = "2021"
746
747[dependencies]
748serde = {{ version = "1.0", features = ["derive"] }}
749serde_json = "1.0"
750"#
751        );
752
753        fs::write(cargo_path, cargo_content)?;
754        Ok(())
755    }
756}
757
758#[cfg(test)]
759mod tests {
760    use super::*;
761
762    #[test]
763    fn cargo_toml_uses_relative_foundation_path_for_sibling_crates() {
764        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
765        let output = Path::new(&manifest_dir)
766            .parent()
767            .unwrap()
768            .join("rh-hl7_fhir_r4_core");
769        let toml = generate_cargo_toml(
770            "hl7.fhir.r4.core",
771            "4.0.1",
772            &output,
773            Some("rh-hl7-fhir-r4-core"),
774        );
775        assert!(toml.contains(r#"rh-foundation = { path = "../rh-foundation""#));
776    }
777
778    #[test]
779    fn cargo_toml_never_contains_absolute_paths() {
780        let toml = generate_cargo_toml(
781            "hl7.fhir.r5.core",
782            "5.0.0",
783            Path::new("/nonexistent/output/dir"),
784            Some("rh-hl7-fhir-r5-core"),
785        );
786        assert!(toml.contains(r#"path = "../rh-foundation""#));
787        for line in toml.lines() {
788            assert!(
789                !line.contains("path = \"/"),
790                "absolute path emitted: {line}"
791            );
792        }
793    }
794
795    #[test]
796    fn cargo_toml_inherits_workspace_metadata() {
797        let toml = generate_cargo_toml("hl7.fhir.r4.core", "4.0.1", Path::new("."), None);
798        for key in [
799            "version.workspace = true",
800            "edition.workspace = true",
801            "license.workspace = true",
802            "repository.workspace = true",
803            "rust-version.workspace = true",
804            "authors.workspace = true",
805        ] {
806            assert!(toml.contains(key), "missing workspace inheritance: {key}");
807        }
808        assert!(toml.contains(r#""fhir-r4""#));
809    }
810}