use std::fmt::Write as _;
use super::naming;
use super::plan::{ChoicePlan, FieldPlan, StructPlan, TypePlan, Wrapper};
use super::version::Version;
const STRUCT_DERIVES: &str = "Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, Validate";
const STRUCT_DERIVES_NO_DEFAULT: &str = "Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Validate";
#[must_use]
pub fn render_type(plan: &TypePlan, version: Version) -> String {
let mut out = String::new();
out.push_str(&module_header(plan, version));
out.push_str(&imports(plan, version));
for structure in &plan.structs {
out.push('\n');
out.push_str(&render_struct(structure, plan, version));
}
for choice in &plan.choices {
out.push('\n');
out.push_str(&render_choice(choice, version));
}
if let Some(root) = plan.structs.first()
&& root.has_default
{
out.push('\n');
out.push_str(&render_tests(&root.name));
}
out
}
fn module_header(plan: &TypePlan, version: Version) -> String {
let mut out = format!("//! {}\n//!\n", plan.type_name);
let _ = write!(out, "//! URL: {}\n//!\n", plan.url);
let _ = write!(out, "//! Version: {}\n//!\n", plan.version);
if !plan.short.is_empty() {
out.push_str(&naming::module_doc_comment(&plan.short));
out.push_str("//!\n");
}
let _ = writeln!(out, "//! FHIR {}: <{}>", version.label(), version.spec_url());
out
}
fn imports(plan: &TypePlan, version: Version) -> String {
let module = version.module();
let derives = if plan.structs.iter().any(|s| s.is_root) {
"use fhir_derive_macros::{Builder, Validate};\n"
} else {
"use fhir_derive_macros::Validate;\n"
};
format!(
"\n// The `types` import is unused by a handful of types that have only \
primitive fields.\n#![allow(unused_imports)]\n\n\
use crate::{module}::types;\n\
use ::serde::{{Deserialize, Serialize}};\n{derives}"
)
}
fn render_struct(structure: &StructPlan, plan: &TypePlan, version: Version) -> String {
let mut out = String::new();
let doc = if structure.is_root && !plan.description.is_empty() {
&plan.description
} else {
&structure.doc
};
out.push_str(&naming::doc_comment(doc, ""));
out.push_str(&example_doctest(&structure.name, plan, version, structure.has_default));
out.push_str("#[serde_with::skip_serializing_none]\n");
let derives = if structure.has_default { STRUCT_DERIVES } else { STRUCT_DERIVES_NO_DEFAULT };
let builder = if structure.is_root { ", Builder" } else { "" };
let _ = writeln!(out, "#[derive({derives}{builder})]");
out.push_str("#[serde(rename_all = \"camelCase\")]\n");
out.push_str(&version_attribute(version));
let _ = writeln!(out, "pub struct {} {{", structure.name);
for (index, field) in structure.fields.iter().enumerate() {
if index > 0 {
out.push('\n');
}
out.push_str(&render_field(field));
}
out.push_str("}\n");
out
}
fn example_doctest(
name: &str,
plan: &TypePlan,
version: Version,
has_default: bool,
) -> String {
let module = version.module();
let group = if plan.is_resource { "resources" } else { "types" };
let fence = if has_default { "```" } else { "```ignore" };
format!(
"///\n\
/// # Examples\n\
///\n\
/// {fence}\n\
/// use fhir::{module}::{group}::{}::{name};\n\
///\n\
/// let value = {name}::default();\n\
/// let json = ::serde_json::to_value(&value).unwrap();\n\
/// let back: {name} = ::serde_json::from_value(json).unwrap();\n\
/// assert_eq!(value, back);\n\
/// ```\n",
plan.module,
)
}
#[must_use]
pub fn version_attribute(version: Version) -> String {
match version {
Version::R5 => String::new(),
other => format!("#[fhir_version(\"{}\")]\n", other.module()),
}
}
fn render_field(field: &FieldPlan) -> String {
let mut out = String::new();
out.push_str(&naming::doc_comment(&field.doc, " "));
if let Some(choice) = &field.choice {
let cardinality = if field.min >= 1 {
format!(
" /// The `{}` choice element ({}..{}); see [`{choice}`]. It is \
`Option` even though the specification makes it mandatory, because \
a choice enum has no default.\n",
field.path, field.min, field.max
)
} else {
format!(
" /// The `{}` choice element ({}..{}); see [`{choice}`].\n",
field.path, field.min, field.max
)
};
out.push_str(&cardinality);
out.push_str(" #[serde(flatten)]\n");
let _ = writeln!(out, " pub {}: Option<{choice}>,", field.ident);
return out;
}
let inner = if field.boxed {
format!("Box<{}>", field.inner_type)
} else {
field.inner_type.clone()
};
if naming::needs_explicit_rename(&field.ident, &field.wire) {
let _ = writeln!(out, " #[serde(rename = {:?})]", field.wire);
}
match field.wrapper {
Wrapper::Option => {
let _ = writeln!(out, " pub {}: Option<{inner}>,", field.ident);
}
Wrapper::Required => {
let _ = writeln!(out, " pub {}: {inner},", field.ident);
}
Wrapper::Vec => {
out.push_str(" #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n");
let _ = writeln!(out, " pub {}: Vec<{inner}>,", field.ident);
}
Wrapper::Vec1 => {
let _ = writeln!(out, " pub {}: ::vec1::Vec1<{inner}>,", field.ident);
}
}
if let Some(sibling) = &field.sibling {
let _ = write!(
out,
" /// Primitive extension sibling for [`{}`](Self::{}) (FHIR `{}`):\n\
\x20 /// carries `id` and/or `extension` for the primitive value.\n\
\x20 #[serde(rename = \"{}\")]\n",
field.ident, field.ident, sibling.wire, sibling.wire,
);
if sibling.is_multiple {
out.push_str(" #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n");
let _ = writeln!(out, " pub {}: Vec<Option<types::Element>>,", sibling.ident);
} else {
let _ = writeln!(out, " pub {}: Option<types::Element>,", sibling.ident);
}
}
out
}
fn render_choice(choice: &ChoicePlan, version: Version) -> String {
let mut out = String::new();
let _ = writeln!(
out,
"/// The `{}` choice element (see `spec/11-choice-types.md`).",
choice.path
);
out.push_str(
"#[derive(Debug, Clone, PartialEq, Eq, fhir_derive_macros::FhirChoice, Validate)]\n",
);
out.push_str(&version_attribute(version));
out.push_str("#[allow(clippy::large_enum_variant)]\n");
let _ = writeln!(out, "pub enum {} {{", choice.name);
for variant in &choice.variants {
let _ = writeln!(out, " /// `{}` variant.", variant.key);
let _ = writeln!(out, " #[fhir(\"{}\")]", variant.key);
let _ = writeln!(out, " {}({}),", variant.name, variant.payload);
}
out.push_str("}\n");
out
}
fn render_tests(name: &str) -> String {
format!(
"#[cfg(test)]\n\
mod tests {{\n\
\x20 use super::*;\n\
\x20 type T = {name};\n\
\n\
\x20 #[test]\n\
\x20 fn test_default() {{\n\
\x20 let _ = T::default();\n\
\x20 }}\n\
\n\
\x20 #[test]\n\
\x20 fn test_serde_round_trip() {{\n\
\x20 let value = T::default();\n\
\x20 let json = ::serde_json::to_value(&value).expect(\"to_value\");\n\
\x20 let back: T = ::serde_json::from_value(json).expect(\"from_value\");\n\
\x20 assert_eq!(value, back);\n\
\x20 }}\n\
}}\n"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::plan::{plan_type, Context};
use crate::codegen::spec::StructureDefinition;
fn ctx() -> Context {
Context {
primitives: ["string", "boolean", "dateTime"].into_iter().map(str::to_string).collect(),
code_enums: std::collections::BTreeSet::new(),
module: "r4".to_string(),
}
}
fn sample_plan() -> TypePlan {
let sd: StructureDefinition = ::serde_json::from_value(::serde_json::json!({
"name": "Sample", "type": "Sample", "kind": "resource",
"url": "http://example.org/Sample", "version": "4.0.1",
"description": "A sample resource.",
"snapshot": { "element": [
{ "path": "Sample", "short": "A sample" },
{ "path": "Sample.note", "min": 0, "max": "1", "short": "A note",
"type": [{ "code": "string" }] },
{ "path": "Sample.tag", "min": 0, "max": "*", "short": "Tags",
"type": [{ "code": "CodeableConcept" }] },
{ "path": "Sample.value[x]", "min": 0, "max": "1", "short": "The value",
"type": [{ "code": "Quantity" }, { "code": "string" }] },
]}
}))
.unwrap();
plan_type(&sd, &ctx()).unwrap()
}
#[test]
fn header_names_the_release() {
let out = render_type(&sample_plan(), Version::R4);
assert!(out.starts_with("//! Sample\n"));
assert!(out.contains("//! URL: http://example.org/Sample"));
assert!(out.contains("//! Version: 4.0.1"));
assert!(out.contains("//! FHIR R4: <https://hl7.org/fhir/R4/>"));
assert!(out.contains("use crate::r4::types;"));
}
#[test]
fn struct_follows_the_conventions() {
let out = render_type(&sample_plan(), Version::R4);
assert!(out.contains("#[serde_with::skip_serializing_none]\n"));
assert!(out.contains(
"#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, Validate, Builder)]"
));
assert!(out.contains("#[serde(rename_all = \"camelCase\")]"));
assert!(out.contains("#[fhir_version(\"r4\")]"));
assert!(out.contains("pub struct Sample {"));
}
#[test]
fn cardinality_and_siblings_render() {
let out = render_type(&sample_plan(), Version::R4);
assert!(out.contains("pub note: Option<types::String>,"));
assert!(out.contains("#[serde(default, skip_serializing_if = \"Vec::is_empty\")]"));
assert!(out.contains("pub tag: Vec<types::CodeableConcept>,"));
assert!(out.contains("#[serde(rename = \"_note\")]"));
assert!(out.contains("pub note_ext: Option<types::Element>,"));
}
#[test]
fn names_camel_case_cannot_reproduce_are_renamed() {
let sd: StructureDefinition = ::serde_json::from_value(::serde_json::json!({
"name": "Sample", "type": "Sample", "kind": "resource",
"url": "u", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Sample" },
{ "path": "Sample.truthTP", "min": 0, "max": "1",
"type": [{ "code": "string" }] },
{ "path": "Sample.plainName", "min": 0, "max": "1",
"type": [{ "code": "string" }] },
]}
}))
.unwrap();
let out = render_type(&plan_type(&sd, &ctx()).unwrap(), Version::R4);
assert!(out.contains("#[serde(rename = \"truthTP\")]"));
assert!(out.contains("pub truth_tp: Option<types::String>,"));
assert!(out.contains("#[serde(rename = \"_truthTP\")]"));
assert!(!out.contains("#[serde(rename = \"plainName\")]"));
}
#[test]
fn choice_field_is_flattened_and_enum_is_emitted() {
let out = render_type(&sample_plan(), Version::R4);
assert!(out.contains("#[serde(flatten)]"));
assert!(out.contains("pub value: Option<SampleValue>,"));
assert!(out.contains("pub enum SampleValue {"));
assert!(out.contains("#[fhir(\"valueQuantity\")]"));
assert!(out.contains("Quantity(Box<types::Quantity>),"));
assert!(out.contains("String(crate::r4::choice::Primitive<types::String>),"));
}
#[test]
fn r5_needs_no_version_attribute() {
let out = render_type(&sample_plan(), Version::R5);
assert!(!out.contains("#[fhir_version"));
assert!(out.contains("use crate::r5::types;"));
}
#[test]
fn tests_and_doctest_are_emitted_for_defaultable_types() {
let out = render_type(&sample_plan(), Version::R4);
assert!(out.contains("fn test_serde_round_trip()"));
assert!(out.contains("/// use fhir::r4::resources::sample::Sample;"));
assert!(!out.contains("```ignore"));
}
#[test]
fn types_without_a_default_skip_the_tests() {
let sd: StructureDefinition = ::serde_json::from_value(::serde_json::json!({
"name": "Sample", "type": "Sample", "kind": "resource",
"url": "u", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Sample" },
{ "path": "Sample.item", "min": 1, "max": "*",
"type": [{ "code": "CodeableConcept" }] },
]}
}))
.unwrap();
let plan = plan_type(&sd, &ctx()).unwrap();
let out = render_type(&plan, Version::R4);
assert!(out.contains("pub item: ::vec1::Vec1<types::CodeableConcept>,"));
assert!(out.contains("#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Validate, Builder)]"));
assert!(out.contains("```ignore"));
assert!(!out.contains("mod tests"));
}
}