use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use super::naming;
use super::spec::{ElementDefinition, StructureDefinition};
#[derive(Debug, Clone)]
pub struct TypePlan {
pub type_name: String,
pub module: String,
pub url: String,
pub version: String,
pub short: String,
pub description: String,
pub structs: Vec<StructPlan>,
pub choices: Vec<ChoicePlan>,
pub is_resource: bool,
}
#[derive(Debug, Clone)]
pub struct StructPlan {
pub name: String,
pub path: String,
pub doc: String,
pub fields: Vec<FieldPlan>,
pub is_root: bool,
pub has_default: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Wrapper {
Option,
Required,
Vec,
Vec1,
}
#[derive(Debug, Clone)]
pub struct FieldPlan {
pub ident: String,
pub wire: String,
pub path: String,
pub doc: String,
pub inner_type: String,
pub wrapper: Wrapper,
pub boxed: bool,
pub min: u32,
pub max: String,
pub choice: Option<String>,
pub sibling: Option<SiblingPlan>,
}
#[derive(Debug, Clone)]
pub struct SiblingPlan {
pub ident: String,
pub wire: String,
pub is_multiple: bool,
}
#[derive(Debug, Clone)]
pub struct ChoicePlan {
pub name: String,
pub path: String,
pub variants: Vec<ChoiceVariant>,
}
#[derive(Debug, Clone)]
pub struct ChoiceVariant {
pub name: String,
pub key: String,
pub payload: String,
}
#[derive(Debug, Clone, Default)]
pub struct Context {
pub primitives: BTreeSet<String>,
pub code_enums: BTreeSet<String>,
pub module: String,
}
impl Context {
#[must_use]
pub fn is_primitive(&self, code: &str) -> bool {
self.primitives.contains(code)
}
#[must_use]
pub fn code_enum_for(&self, value_set: &str) -> Option<String> {
let name = value_set.split('|').next()?.rsplit('/').next()?;
let candidate = naming::enum_name(name);
self.code_enums.contains(&candidate).then_some(candidate)
}
}
#[must_use]
pub fn plan_type(sd: &StructureDefinition, ctx: &Context) -> Option<TypePlan> {
let snapshot = sd.snapshot.as_ref()?;
if sd.kind == "primitive-type" {
return None;
}
let elements = &snapshot.element;
let root = elements.first()?;
let rust_name = naming::pascal(&sd.name);
let backbones = backbone_paths(elements, &sd.type_name);
let mut structs: Vec<StructPlan> = Vec::new();
let mut choices: Vec<ChoicePlan> = Vec::new();
let rename = Rename { fhir_type: sd.type_name.clone(), rust_name: rust_name.clone() };
let mut owners: Vec<&str> = vec![sd.type_name.as_str()];
owners.extend(backbones.iter().map(String::as_str));
for owner in owners {
let owner_doc = if owner == sd.type_name {
element_doc(root, sd.description.as_deref())
} else {
elements
.iter()
.find(|e| e.base_path() == owner)
.map(|e| element_doc(e, None))
.unwrap_or_default()
};
let mut fields = Vec::new();
for element in elements.iter().filter(|e| e.owner_path() == Some(owner)) {
let Some(field) =
plan_field(element, owner, &backbones, ctx, &mut choices, &rename)
else {
continue;
};
fields.push(field);
}
let has_default = !fields.iter().any(|f| f.wrapper == Wrapper::Vec1);
structs.push(StructPlan {
name: struct_name_in(owner, &sd.type_name, &rust_name),
path: owner.to_string(),
doc: owner_doc,
fields,
is_root: owner == sd.type_name,
has_default,
});
}
Some(TypePlan {
type_name: rust_name.clone(),
module: naming::module_name(&sd.name),
url: sd.url.clone(),
version: sd.version.clone().unwrap_or_default(),
short: root.short.clone().unwrap_or_default(),
description: sd.description.clone().unwrap_or_default(),
structs,
choices,
is_resource: sd.kind == "resource",
})
}
struct Rename {
fhir_type: String,
rust_name: String,
}
impl Rename {
fn struct_name(&self, path: &str) -> String {
struct_name_in(path, &self.fhir_type, &self.rust_name)
}
}
fn struct_name_in(path: &str, fhir_type: &str, rust_name: &str) -> String {
let rest = path.strip_prefix(fhir_type).unwrap_or(path);
if rest.is_empty() {
return rust_name.to_string();
}
format!("{rust_name}{}", naming::struct_name(rest.trim_start_matches('.')))
}
fn backbone_paths(elements: &[ElementDefinition], type_name: &str) -> BTreeSet<String> {
let owners: HashSet<&str> = elements.iter().filter_map(ElementDefinition::owner_path).collect();
elements
.iter()
.map(|e| e.base_path().to_string())
.filter(|p| p != type_name && owners.contains(p.as_str()))
.collect()
}
fn element_doc(element: &ElementDefinition, fallback: Option<&str>) -> String {
element
.definition
.clone()
.or_else(|| element.short.clone())
.or_else(|| fallback.map(str::to_string))
.unwrap_or_default()
}
fn plan_field(
element: &ElementDefinition,
owner: &str,
backbones: &BTreeSet<String>,
ctx: &Context,
choices: &mut Vec<ChoicePlan>,
rename: &Rename,
) -> Option<FieldPlan> {
if element.max.as_deref() == Some("0") {
return None;
}
let leaf = element.leaf();
let ident = naming::field_ident(leaf);
let wrapper = wrapper_for(element);
let mut choice = None;
let mut sibling = None;
let inner_type;
if element.is_choice() {
let owner_struct = rename.struct_name(owner);
let name = format!("{owner_struct}{}", naming::pascal(leaf));
choices.push(ChoicePlan {
name: name.clone(),
path: element.path.clone(),
variants: choice_variants(element, leaf, ctx),
});
inner_type = name.clone();
choice = Some(name);
} else if backbones.contains(element.base_path()) {
inner_type = rename.struct_name(element.base_path());
} else if let Some(target) = element.content_reference_path() {
inner_type = rename.struct_name(target);
} else {
let type_code = element.types.first()?.code.as_str();
if type_code.is_empty() {
return None; }
inner_type = scalar_type(type_code, element, ctx);
if ctx.is_primitive(type_code) && !element.is_system_element() {
sibling = Some(SiblingPlan {
ident: format!("{}_ext", naming::snake(leaf)),
wire: format!("_{leaf}"),
is_multiple: element.is_multiple(),
});
}
}
Some(FieldPlan {
ident,
wire: leaf.to_string(),
path: element.path.clone(),
doc: element.short.clone().unwrap_or_default(),
inner_type,
wrapper,
boxed: false,
min: element.min,
max: element.max.clone().unwrap_or_else(|| "1".to_string()),
choice,
sibling,
})
}
fn scalar_type(type_code: &str, element: &ElementDefinition, ctx: &Context) -> String {
if type_code == "code"
&& let Some(binding) = &element.binding
&& binding.strength == "required"
&& let Some(enum_name) = binding.value_set().and_then(|vs| ctx.code_enum_for(vs))
{
let module = &ctx.module;
return format!("crate::coded::Coded<crate::{module}::codes::{enum_name}>");
}
type_reference(type_code)
}
fn type_reference(type_code: &str) -> String {
if type_code == "Resource" || type_code == "DomainResource" {
return "::serde_json::Value".to_string();
}
if let Some(system) = type_code.strip_prefix("http://hl7.org/fhirpath/System.") {
return format!("types::{system}");
}
format!("types::{}", naming::pascal(type_code))
}
fn choice_variants(element: &ElementDefinition, leaf: &str, ctx: &Context) -> Vec<ChoiceVariant> {
let module = &ctx.module;
let mut seen = HashSet::new();
element
.types
.iter()
.filter(|t| seen.insert(t.code.clone()))
.map(|t| {
let payload = if ctx.is_primitive(&t.code) {
format!("crate::{module}::choice::Primitive<{}>", type_reference(&t.code))
} else {
format!("Box<{}>", type_reference(&t.code))
};
ChoiceVariant {
name: naming::pascal(&t.code),
key: naming::choice_key(leaf, &t.code),
payload,
}
})
.collect()
}
fn wrapper_for(element: &ElementDefinition) -> Wrapper {
let multiple = element.is_multiple();
let required = element.min >= 1;
if element.is_choice() {
return Wrapper::Option;
}
match (required, multiple) {
(false, false) => Wrapper::Option,
(true, false) => Wrapper::Required,
(false, true) => Wrapper::Vec,
(true, true) => Wrapper::Vec1,
}
}
pub fn break_type_cycles(plans: &mut [TypePlan]) {
let mut owner_of: HashMap<String, (usize, usize)> = HashMap::new();
for (ti, plan) in plans.iter().enumerate() {
for (si, structure) in plan.structs.iter().enumerate() {
owner_of.insert(structure.name.clone(), (ti, si));
}
}
let edges: BTreeMap<String, Vec<(usize, String)>> = plans
.iter()
.flat_map(|plan| plan.structs.iter())
.map(|structure| {
let targets = structure
.fields
.iter()
.enumerate()
.filter(|(_, f)| matches!(f.wrapper, Wrapper::Option | Wrapper::Required))
.filter_map(|(i, f)| inline_struct_target(f).map(|t| (i, t.to_string())))
.collect();
(structure.name.clone(), targets)
})
.collect();
let mut state: HashMap<&str, u8> = HashMap::new();
let mut to_box: Vec<(String, usize)> = Vec::new();
let names: Vec<&str> = edges.keys().map(String::as_str).collect();
for name in names {
visit(name, &edges, &mut state, &mut to_box);
}
for (struct_name, field_index) in to_box {
if let Some(&(ti, si)) = owner_of.get(&struct_name) {
plans[ti].structs[si].fields[field_index].boxed = true;
}
}
}
fn struct_target(field: &FieldPlan) -> Option<&str> {
if field.choice.is_some() {
return None;
}
if let Some(name) = field.inner_type.strip_prefix("types::") {
return Some(name);
}
field
.inner_type
.starts_with(|c: char| c.is_ascii_uppercase())
.then_some(field.inner_type.as_str())
}
fn inline_struct_target(field: &FieldPlan) -> Option<&str> {
matches!(field.wrapper, Wrapper::Option | Wrapper::Required)
.then(|| struct_target(field))
.flatten()
}
fn visit<'a>(
name: &'a str,
edges: &'a BTreeMap<String, Vec<(usize, String)>>,
state: &mut HashMap<&'a str, u8>,
to_box: &mut Vec<(String, usize)>,
) {
if state.contains_key(name) {
return;
}
state.insert(name, 1);
if let Some(targets) = edges.get(name) {
for (field_index, target) in targets {
match state.get(target.as_str()) {
Some(1) => to_box.push((name.to_string(), *field_index)),
Some(_) => {}
None => visit(target, edges, state, to_box),
}
}
}
state.insert(name, 2);
}
pub fn resolve_defaults(plans: &mut [TypePlan]) {
let mut without_default: BTreeSet<String> = plans
.iter()
.flat_map(|p| p.structs.iter())
.filter(|s| !s.has_default)
.map(|s| s.name.clone())
.collect();
loop {
let mut added = false;
for plan in plans.iter() {
for structure in &plan.structs {
if without_default.contains(&structure.name) {
continue;
}
let inherits = structure.fields.iter().any(|f| {
f.wrapper == Wrapper::Required
&& struct_target(f).is_some_and(|t| without_default.contains(t))
});
if inherits {
without_default.insert(structure.name.clone());
added = true;
}
}
}
if !added {
break;
}
}
for plan in plans.iter_mut() {
for structure in &mut plan.structs {
structure.has_default = !without_default.contains(&structure.name);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::spec::StructureDefinition;
fn ctx() -> Context {
Context {
primitives: ["string", "boolean", "dateTime", "code", "xhtml"]
.into_iter()
.map(str::to_string)
.collect(),
code_enums: ["ObservationStatus"].into_iter().map(str::to_string).collect(),
module: "r4".to_string(),
}
}
fn sd(json: ::serde_json::Value) -> StructureDefinition {
::serde_json::from_value(json).unwrap()
}
#[test]
fn cardinality_maps_to_wrappers() {
let plan = plan_type(
&sd(::serde_json::json!({
"name": "Sample", "type": "Sample", "kind": "complex-type",
"url": "http://example.org/Sample", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Sample" },
{ "path": "Sample.a", "min": 0, "max": "1", "type": [{ "code": "Period" }] },
{ "path": "Sample.b", "min": 1, "max": "1", "type": [{ "code": "Period" }] },
{ "path": "Sample.c", "min": 0, "max": "*", "type": [{ "code": "Period" }] },
{ "path": "Sample.d", "min": 1, "max": "*", "type": [{ "code": "Period" }] },
]}
})),
&ctx(),
)
.unwrap();
let root = &plan.structs[0];
let wrappers: Vec<Wrapper> = root.fields.iter().map(|f| f.wrapper).collect();
assert_eq!(
wrappers,
[Wrapper::Option, Wrapper::Required, Wrapper::Vec, Wrapper::Vec1]
);
assert!(!root.has_default);
}
#[test]
fn backbone_elements_become_nested_structs() {
let plan = plan_type(
&sd(::serde_json::json!({
"name": "Obs", "type": "Obs", "kind": "resource",
"url": "http://example.org/Obs", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Obs" },
{ "path": "Obs.component", "min": 0, "max": "*",
"type": [{ "code": "BackboneElement" }] },
{ "path": "Obs.component.code", "min": 1, "max": "1",
"type": [{ "code": "CodeableConcept" }] },
]}
})),
&ctx(),
)
.unwrap();
assert_eq!(plan.structs.len(), 2);
assert_eq!(plan.structs[1].name, "ObsComponent");
assert_eq!(plan.structs[0].fields[0].inner_type, "ObsComponent");
assert_eq!(plan.structs[1].fields[0].inner_type, "types::CodeableConcept");
}
#[test]
fn content_reference_reuses_another_backbone() {
let plan = plan_type(
&sd(::serde_json::json!({
"name": "Obs", "type": "Obs", "kind": "resource",
"url": "http://example.org/Obs", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Obs" },
{ "path": "Obs.referenceRange", "min": 0, "max": "*",
"type": [{ "code": "BackboneElement" }] },
{ "path": "Obs.referenceRange.low", "min": 0, "max": "1",
"type": [{ "code": "Quantity" }] },
{ "path": "Obs.component", "min": 0, "max": "*",
"type": [{ "code": "BackboneElement" }] },
{ "path": "Obs.component.referenceRange", "min": 0, "max": "*",
"contentReference": "#Obs.referenceRange" },
]}
})),
&ctx(),
)
.unwrap();
let component = plan.structs.iter().find(|s| s.name == "ObsComponent").unwrap();
assert_eq!(component.fields[0].inner_type, "ObsReferenceRange");
}
#[test]
fn choice_elements_become_enums() {
let plan = plan_type(
&sd(::serde_json::json!({
"name": "Obs", "type": "Obs", "kind": "resource",
"url": "http://example.org/Obs", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Obs" },
{ "path": "Obs.value[x]", "min": 0, "max": "1",
"type": [{ "code": "Quantity" }, { "code": "string" }] },
]}
})),
&ctx(),
)
.unwrap();
assert_eq!(plan.choices.len(), 1);
let choice = &plan.choices[0];
assert_eq!(choice.name, "ObsValue");
assert_eq!(choice.variants[0].key, "valueQuantity");
assert_eq!(choice.variants[0].payload, "Box<types::Quantity>");
assert_eq!(choice.variants[1].key, "valueString");
assert_eq!(choice.variants[1].payload, "crate::r4::choice::Primitive<types::String>");
assert_eq!(plan.structs[0].fields[0].choice.as_deref(), Some("ObsValue"));
}
#[test]
fn required_bindings_become_coded_enums() {
let plan = plan_type(
&sd(::serde_json::json!({
"name": "Obs", "type": "Obs", "kind": "resource",
"url": "http://example.org/Obs", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Obs" },
{ "path": "Obs.status", "min": 1, "max": "1", "type": [{ "code": "code" }],
"binding": { "strength": "required",
"valueSet": "http://hl7.org/fhir/ValueSet/observation-status|4.0.1" } },
{ "path": "Obs.other", "min": 0, "max": "1", "type": [{ "code": "code" }],
"binding": { "strength": "required",
"valueSet": "http://hl7.org/fhir/ValueSet/not-generated" } },
]}
})),
&ctx(),
)
.unwrap();
assert_eq!(
plan.structs[0].fields[0].inner_type,
"crate::coded::Coded<crate::r4::codes::ObservationStatus>"
);
assert_eq!(plan.structs[0].fields[1].inner_type, "types::Code");
}
#[test]
fn primitive_fields_get_extension_siblings() {
let plan = plan_type(
&sd(::serde_json::json!({
"name": "Pat", "type": "Pat", "kind": "resource",
"url": "http://example.org/Pat", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Pat" },
{ "path": "Pat.id", "min": 0, "max": "1",
"type": [{ "code": "http://hl7.org/fhirpath/System.String" }] },
{ "path": "Pat.birthDate", "min": 0, "max": "1",
"type": [{ "code": "dateTime" }] },
{ "path": "Pat.given", "min": 0, "max": "*", "type": [{ "code": "string" }] },
{ "path": "Pat.link", "min": 0, "max": "1", "type": [{ "code": "Reference" }] },
]}
})),
&ctx(),
)
.unwrap();
let fields = &plan.structs[0].fields;
assert!(fields[0].sibling.is_none());
assert_eq!(fields[0].inner_type, "types::String");
let birth = fields[1].sibling.as_ref().unwrap();
assert_eq!(birth.ident, "birth_date_ext");
assert_eq!(birth.wire, "_birthDate");
assert!(!birth.is_multiple);
assert!(fields[2].sibling.as_ref().unwrap().is_multiple);
assert!(fields[3].sibling.is_none());
}
#[test]
fn removed_elements_are_skipped() {
let plan = plan_type(
&sd(::serde_json::json!({
"name": "Sample", "type": "Sample", "kind": "complex-type",
"url": "http://example.org/Sample", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Sample" },
{ "path": "Sample.gone", "min": 0, "max": "0", "type": [{ "code": "string" }] },
{ "path": "Sample.kept", "min": 0, "max": "1", "type": [{ "code": "string" }] },
]}
})),
&ctx(),
)
.unwrap();
let idents: Vec<&str> = plan.structs[0].fields.iter().map(|f| f.ident.as_str()).collect();
assert_eq!(idents, ["kept"]);
}
#[test]
fn polymorphic_slots_stay_raw_json() {
assert_eq!(type_reference("Resource"), "::serde_json::Value");
assert_eq!(type_reference("DomainResource"), "::serde_json::Value");
assert_eq!(type_reference("http://hl7.org/fhirpath/System.String"), "types::String");
assert_eq!(type_reference("CodeableConcept"), "types::CodeableConcept");
assert_eq!(type_reference("dateTime"), "types::DateTime");
}
#[test]
fn cycles_are_broken_with_a_box() {
let mut plans = vec![
plan_type(
&sd(::serde_json::json!({
"name": "Identifier", "type": "Identifier", "kind": "complex-type",
"url": "u", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Identifier" },
{ "path": "Identifier.assigner", "min": 0, "max": "1",
"type": [{ "code": "Reference" }] },
]}
})),
&ctx(),
)
.unwrap(),
plan_type(
&sd(::serde_json::json!({
"name": "Reference", "type": "Reference", "kind": "complex-type",
"url": "u", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Reference" },
{ "path": "Reference.identifier", "min": 0, "max": "1",
"type": [{ "code": "Identifier" }] },
]}
})),
&ctx(),
)
.unwrap(),
];
break_type_cycles(&mut plans);
let boxed: Vec<bool> = plans
.iter()
.flat_map(|p| p.structs.iter())
.flat_map(|s| s.fields.iter())
.map(|f| f.boxed)
.collect();
assert_eq!(boxed.iter().filter(|b| **b).count(), 1, "{boxed:?}");
}
#[test]
fn repeating_fields_never_need_a_box() {
let mut plans = vec![
plan_type(
&sd(::serde_json::json!({
"name": "Extension", "type": "Extension", "kind": "complex-type",
"url": "u", "version": "4.0.1",
"snapshot": { "element": [
{ "path": "Extension" },
{ "path": "Extension.extension", "min": 0, "max": "*",
"type": [{ "code": "Extension" }] },
]}
})),
&ctx(),
)
.unwrap(),
];
break_type_cycles(&mut plans);
assert!(!plans[0].structs[0].fields[0].boxed);
}
}