use std::fmt::Write as _;
use std::path::Path;
pub mod codes_gen;
pub mod extension_ext_gen;
pub mod meta_gen;
pub mod naming;
pub mod plan;
pub mod primitives;
pub mod render;
pub mod spec;
pub mod version;
pub use version::Version;
pub fn generate(version: Version) -> std::io::Result<Summary> {
generate_into(version, &version.source_dir())
}
pub fn generate_into(version: Version, out_root: &Path) -> std::io::Result<Summary> {
let type_definitions = spec::read_structure_definitions(&version.types_bundle())?;
let resource_definitions = spec::read_structure_definitions(&version.resources_bundle())?;
let mut systems = Vec::new();
for bundle in version.code_system_bundles() {
systems.extend(spec::read_code_systems(&bundle)?);
}
let code_enums = codes_gen::plan(&systems);
let primitive_defs = primitives::codes_of(&type_definitions);
let ctx = plan::Context {
primitives: primitive_defs.keys().cloned().collect(),
code_enums: code_enums.iter().map(|e| e.name.clone()).collect(),
module: version.module().to_string(),
};
let mut type_plans: Vec<plan::TypePlan> = type_definitions
.iter()
.filter(|sd| sd.kind == "complex-type")
.filter_map(|sd| plan::plan_type(sd, &ctx))
.collect();
let mut resource_plans: Vec<plan::TypePlan> = resource_definitions
.iter()
.filter(|sd| sd.kind == "resource" && !sd.is_abstract)
.filter_map(|sd| plan::plan_type(sd, &ctx))
.collect();
let mut all: Vec<plan::TypePlan> =
type_plans.iter().chain(resource_plans.iter()).cloned().collect();
plan::break_type_cycles(&mut all);
plan::resolve_defaults(&mut all);
let (planned_types, planned_resources) = all.split_at(type_plans.len());
type_plans = planned_types.to_vec();
resource_plans = planned_resources.to_vec();
type_plans.sort_by(|a, b| a.type_name.cmp(&b.type_name));
resource_plans.sort_by(|a, b| a.type_name.cmp(&b.type_name));
let root = out_root.to_path_buf();
let types_dir = root.join("types");
let resources_dir = root.join("resources");
std::fs::create_dir_all(&types_dir)?;
std::fs::create_dir_all(&resources_dir)?;
let mut primitive_names = Vec::new();
for (name, sd) in &primitive_defs {
let Some(source) = primitives::render(sd, version) else {
return Err(std::io::Error::other(format!(
"no Rust representation is defined for the FHIR primitive {name:?}; \
add one to codegen::primitives::PRIMITIVES"
)));
};
write_if_changed(&types_dir.join(format!("{}.rs", naming::module_name(name))), &source)?;
primitive_names.push(naming::pascal(name));
}
for plan in &type_plans {
let source = render::render_type(plan, version);
write_if_changed(&types_dir.join(format!("{}.rs", plan.module)), &source)?;
}
for plan in &resource_plans {
let source = render::render_type(plan, version);
write_if_changed(&resources_dir.join(format!("{}.rs", plan.module)), &source)?;
}
write_if_changed(
&root.join("types.rs"),
&render_types_module(&primitive_defs.keys().cloned().collect::<Vec<_>>(), &type_plans, version),
)?;
write_if_changed(
&root.join("resources.rs"),
&render_resources_module(&resource_plans, version),
)?;
write_if_changed(&root.join("codes.rs"), &codes_gen::render(&code_enums, version))?;
write_if_changed(
&root.join("extension_ext.rs"),
&extension_ext_gen::render(&type_plans, &resource_plans, version),
)?;
let meta_table = meta_gen::collect(version)?;
std::fs::create_dir_all(root.join("meta"))?;
write_if_changed(
&root.join("meta").join("generated.rs"),
&meta_gen::render(&meta_table, version),
)?;
Ok(Summary {
version,
primitives: primitive_defs.len(),
datatypes: type_plans.len(),
resources: resource_plans.len(),
code_enums: code_enums.len(),
elements: meta_table.len(),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Summary {
pub version: Version,
pub primitives: usize,
pub datatypes: usize,
pub resources: usize,
pub code_enums: usize,
pub elements: usize,
}
impl std::fmt::Display for Summary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: {} primitives, {} datatypes, {} resources, {} code enums, {} elements",
self.version.label(),
self.primitives,
self.datatypes,
self.resources,
self.code_enums,
self.elements,
)
}
}
fn write_if_changed(path: &Path, source: &str) -> std::io::Result<()> {
if std::fs::read_to_string(path).is_ok_and(|existing| existing == source) {
return Ok(());
}
std::fs::write(path, source)
}
fn render_types_module(
primitive_names: &[String],
plans: &[plan::TypePlan],
version: Version,
) -> String {
let label = version.label();
let module = version.module();
let mut names: Vec<(String, String)> = primitive_names
.iter()
.map(|n| (naming::module_name(n), naming::pascal(n)))
.chain(plans.iter().map(|p| (p.module.clone(), p.type_name.clone())))
.collect();
names.sort();
let mut out = format!(
"//! FHIR {label} datatypes.\n\
//!\n\
//! This module contains every FHIR {label} datatype: the **complex** datatypes\n\
//! (structs such as [`Period`], [`HumanName`], [`CodeableConcept`], [`Coding`])\n\
//! and the **primitive** datatypes (transparent newtypes such as [`Code`],\n\
//! [`Id`], [`DateTime`], [`Boolean`]).\n\
//!\n\
//! Each datatype derives `serde::Serialize` and `serde::Deserialize` and\n\
//! round-trips to and from the canonical FHIR JSON representation. Primitives\n\
//! serialize *transparently* to their underlying JSON value:\n\
//!\n\
//! ```\n\
//! use fhir::{module}::types::Code;\n\
//!\n\
//! assert_eq!(serde_json::to_value(Code(\"final\".to_string())).unwrap(), \"final\");\n\
//! ```\n\
//!\n\
//! Every datatype is re-exported at this module's root, so you can write\n\
//! `fhir::{module}::types::Period` rather than `fhir::{module}::types::period::Period`.\n\
\n"
);
for (module_name, _) in &names {
let _ = writeln!(out, "pub mod {module_name};");
}
out.push('\n');
for (module_name, type_name) in &names {
let _ = writeln!(out, "pub use {module_name}::{type_name};");
}
out
}
fn render_resources_module(plans: &[plan::TypePlan], version: Version) -> String {
let label = version.label();
let module = version.module();
let mut out = format!(
"//! FHIR {label} resources.\n\
//!\n\
//! This module contains the FHIR {label} resource types (Patient, Observation,\n\
//! Encounter, and so on). Each resource is a Rust struct that serializes to\n\
//! and from the canonical FHIR JSON representation via `serde`.\n\
\n\
use ::serde::{{Deserialize, Serialize}};\n\
use fhir_derive_macros::Validate;\n\
\n"
);
for plan in plans {
let _ = writeln!(out, "pub mod {};", plan.module);
}
out.push('\n');
for plan in plans {
let _ = writeln!(out, "pub use {}::{};", plan.module, plan.type_name);
}
let example = plans
.iter()
.find(|p| p.type_name == "Patient")
.or_else(|| plans.first())
.map(|p| p.type_name.clone())
.unwrap_or_default();
let _ = write!(
out,
"\n/// Any FHIR {label} resource, tagged by its `resourceType`.\n\
///\n\
/// Used wherever a resource of any type may appear — for example a\n\
/// `Bundle.entry.resource` or a `contained` resource. Serde reads and writes\n\
/// the `resourceType` discriminator automatically.\n\
///\n\
/// # Examples\n\
///\n\
/// ```\n\
/// use fhir::{module}::resources::Resource;\n\
///\n\
/// let json = ::serde_json::json!({{\"resourceType\": \"{example}\"}});\n\
/// let resource: Resource = ::serde_json::from_value(json).unwrap();\n\
/// assert!(matches!(resource, Resource::{example}(_)));\n\
/// ```\n\
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Validate)]\n\
#[serde(tag = \"resourceType\")]\n"
);
out.push_str(&render::version_attribute(version));
out.push_str("pub enum Resource {\n");
for plan in plans {
let _ = writeln!(
out,
" {}(Box<{}::{}>),",
plan.type_name, plan.module, plan.type_name
);
}
out.push_str("}\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn types_module_declares_and_reexports_each_datatype() {
let plans = vec![];
let out = render_types_module(&["string".to_string()], &plans, Version::R4);
assert!(out.contains("pub mod string;"));
assert!(out.contains("pub use string::String;"));
assert!(out.contains("//! FHIR R4 datatypes."));
assert!(out.contains("use fhir::r4::types::Code;"));
}
#[test]
fn resources_module_builds_the_tagged_enum() {
let sd: spec::StructureDefinition = ::serde_json::from_value(::serde_json::json!({
"name": "Patient", "type": "Patient", "kind": "resource",
"url": "u", "version": "4.0.1",
"snapshot": { "element": [{ "path": "Patient" }] }
}))
.unwrap();
let plans = vec![plan::plan_type(&sd, &plan::Context::default()).unwrap()];
let out = render_resources_module(&plans, Version::R4);
assert!(out.contains("pub mod patient;"));
assert!(out.contains("pub use patient::Patient;"));
assert!(out.contains("#[serde(tag = \"resourceType\")]"));
assert!(out.contains("Patient(Box<patient::Patient>),"));
assert!(out.contains("#[fhir_version(\"r4\")]"));
}
#[test]
fn summary_reads_as_a_sentence() {
let summary = Summary {
version: Version::R4,
primitives: 20,
datatypes: 43,
resources: 146,
code_enums: 400,
elements: 5000,
};
assert_eq!(
summary.to_string(),
"R4: 20 primitives, 43 datatypes, 146 resources, 400 code enums, 5000 elements"
);
}
}