use std::collections::HashMap;
use std::path::PathBuf;
use ::convert_case::{Case, Casing};
use crate::r5::meta;
const PRIMITIVES: &[&str] = &[
"base64Binary",
"boolean",
"canonical",
"code",
"date",
"dateTime",
"decimal",
"id",
"instant",
"integer",
"integer64",
"markdown",
"oid",
"positiveInt",
"string",
"time",
"unsignedInt",
"uri",
"url",
"uuid",
"xhtml",
];
#[derive(Debug, Clone)]
pub struct Insertion {
pub struct_name: String,
pub path: String,
pub field: String,
pub sibling: String,
pub json_key: String,
pub repeating: bool,
pub after_line: usize,
}
fn crate_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn struct_path_prefixes() -> HashMap<String, String> {
let mut map = HashMap::new();
for el in meta::elements() {
let segments: Vec<&str> = el.path.split('.').collect();
for take in 1..segments.len() {
let prefix_segments = &segments[..take];
let struct_name: String = prefix_segments
.iter()
.map(|s| s.to_case(Case::Pascal))
.collect();
let path_prefix = prefix_segments.join(".");
map.entry(struct_name).or_insert(path_prefix);
}
}
map
}
fn is_primitive(el: &meta::ElementMeta) -> bool {
el.types.len() == 1 && PRIMITIVES.contains(&el.types[0].code)
}
fn parse_field_name(line: &str) -> Option<&str> {
let t = line.trim_start();
let rest = t.strip_prefix("pub ")?;
let colon = rest.find(':')?;
let name = rest[..colon].trim();
let bare = name.strip_prefix("r#").unwrap_or(name);
if bare.is_empty() || !bare.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return None;
}
Some(name)
}
fn struct_spans(lines: &[&str]) -> Vec<(String, usize, usize)> {
let mut spans = Vec::new();
let mut i = 0;
while i < lines.len() {
let t = lines[i].trim_start();
if let Some(rest) = t.strip_prefix("pub struct ") {
let name: String = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
let mut depth = 0i32;
let mut j = i;
let mut started = false;
while j < lines.len() {
for c in lines[j].chars() {
if c == '{' {
depth += 1;
started = true;
} else if c == '}' {
depth -= 1;
}
}
if started && depth == 0 {
break;
}
j += 1;
}
spans.push((name, i, j));
i = j + 1;
} else {
i += 1;
}
}
spans
}
#[must_use]
pub fn plan_file(text: &str, skipped: &mut Vec<(String, String)>) -> Vec<Insertion> {
let lines: Vec<&str> = text.lines().collect();
let prefixes = struct_path_prefixes();
let mut insertions = Vec::new();
for (struct_name, start, end) in struct_spans(&lines) {
let Some(prefix) = prefixes.get(&struct_name) else {
continue; };
let mut existing_siblings = std::collections::HashSet::new();
for line in &lines[start..=end] {
if let Some(idx) = line.find("rename = \"_") {
let rest = &line[idx + "rename = \"".len()..];
if let Some(q) = rest.find('"') {
existing_siblings.insert(rest[..q].to_string());
}
}
}
for (offset, line) in lines[start..=end].iter().enumerate() {
let line_no = start + offset;
let Some(field) = parse_field_name(line) else {
continue;
};
let bare = field.strip_prefix("r#").unwrap_or(field);
if bare == "id" {
continue; }
let json_name = bare.to_case(Case::Camel);
let path = format!("{prefix}.{json_name}");
let Some(el) = meta::element(&path) else {
skipped.push((struct_name.clone(), field.to_string()));
continue;
};
if !is_primitive(el) {
continue;
}
let json_key = format!("_{json_name}");
if existing_siblings.contains(&json_key) {
continue; }
insertions.push(Insertion {
struct_name: struct_name.clone(),
path,
field: field.to_string(), sibling: format!("{bare}_ext"),
json_key,
repeating: el.is_multiple(),
after_line: line_no,
});
}
}
insertions
}
fn render_sibling(ins: &Insertion) -> String {
let ty = if ins.repeating {
"Option<Vec<Option<types::Element>>>"
} else {
"Option<types::Element>"
};
let display = ins.field.strip_prefix("r#").unwrap_or(&ins.field);
format!(
"\n /// Primitive extension sibling for [`{display}`](Self::{field}) \
(FHIR `{json_key}`).\n #[serde(rename = \"{json_key}\")]\n pub {sibling}: {ty},",
field = ins.field,
json_key = ins.json_key,
sibling = ins.sibling,
)
}
#[must_use]
pub fn apply_insertions(text: &str, mut insertions: Vec<Insertion>) -> String {
let mut lines: Vec<String> = text.lines().map(String::from).collect();
insertions.sort_by_key(|i| std::cmp::Reverse(i.after_line));
for ins in insertions {
lines[ins.after_line].push_str(&render_sibling(&ins));
}
let mut out = lines.join("\n");
if text.ends_with('\n') {
out.push('\n');
}
out
}
#[must_use]
pub fn patch_default_tests<S: std::hash::BuildHasher>(
text: &str,
changed_structs: &std::collections::HashSet<String, S>,
) -> String {
let lines: Vec<&str> = text.lines().collect();
let mut out: Vec<String> = Vec::with_capacity(lines.len());
let mut current_t: Option<String> = None;
let mut i = 0;
while i < lines.len() {
out.push(lines[i].to_string());
if let Some(rest) = lines[i].trim_start().strip_prefix("type T = ") {
current_t = Some(rest.trim_end_matches(';').trim().to_string());
}
let t_changed = current_t.as_ref().is_some_and(|t| changed_structs.contains(t));
if t_changed && lines[i].trim_start().starts_with("let expect = T {") {
let mut j = i + 1;
let mut already = false;
while j < lines.len() && lines[j].trim() != "};" {
if lines[j].contains("..Default::default()") {
already = true;
}
j += 1;
}
if j < lines.len() && !already {
for line in &lines[i + 1..j] {
out.push((*line).to_string());
}
out.push(" ..Default::default()".to_string());
out.push(lines[j].to_string());
i = j + 1;
continue;
}
}
i += 1;
}
let mut joined = out.join("\n");
if text.ends_with('\n') {
joined.push('\n');
}
joined
}
pub fn apply_element_base_group() -> usize {
let element_base = "\n /// Unique id for inter-element referencing\n \
pub id: Option<types::String>,\n\n \
/// Additional content defined by implementations\n \
pub extension: Option<Vec<types::Extension>>,";
let mut total = 0;
for file in group_files("types") {
let text = std::fs::read_to_string(&file).expect("read file");
let lines: Vec<&str> = text.lines().collect();
let Some((name, start, end)) = struct_spans(&lines).into_iter().next() else {
continue;
};
let span = &lines[start..=end];
let is_braced = lines[start].contains('{');
let has_extension = span.iter().any(|l| l.trim_start().starts_with("pub extension:"));
let is_empty = lines[start].contains("{}");
if !is_braced || has_extension || is_empty {
continue;
}
let mut new_lines: Vec<String> = lines.iter().map(|s| (*s).to_string()).collect();
new_lines[start].push_str(element_base);
let spliced = new_lines.join("\n");
let with_ln = if text.ends_with('\n') { format!("{spliced}\n") } else { spliced };
let changed: std::collections::HashSet<String> = std::iter::once(name).collect();
let patched = patch_default_tests(&with_ln, &changed);
if patched != text {
std::fs::write(&file, patched).expect("write file");
total += 1;
}
}
total
}
fn group_files(group: &str) -> Vec<PathBuf> {
let dir = crate_root().join("src").join("r5").join(group);
let mut files: Vec<PathBuf> = std::fs::read_dir(&dir)
.expect("read group dir")
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "rs"))
.collect();
files.sort();
files
}
#[must_use]
pub fn plan_group(group: &str) -> (Vec<Insertion>, Vec<(String, String)>) {
let mut all = Vec::new();
let mut skipped = Vec::new();
for file in group_files(group) {
let text = std::fs::read_to_string(&file).expect("read file");
all.extend(plan_file(&text, &mut skipped));
}
(all, skipped)
}
pub fn apply_group(group: &str) -> usize {
let mut total = 0;
for file in group_files(group) {
let text = std::fs::read_to_string(&file).expect("read file");
let mut skipped = Vec::new();
let insertions = plan_file(&text, &mut skipped);
total += insertions.len();
let changed_structs: std::collections::HashSet<String> =
insertions.iter().map(|i| i.struct_name.clone()).collect();
let spliced = apply_insertions(&text, insertions);
let new_text = patch_default_tests(&spliced, &changed_structs);
if new_text != text {
std::fs::write(&file, new_text).expect("write file");
}
}
total
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_field_names() {
assert_eq!(parse_field_name(" pub family: Option<types::String>,"), Some("family"));
assert_eq!(parse_field_name(" pub given: Vec<types::String>, // « C »"), Some("given"));
assert_eq!(parse_field_name(" pub r#type: types::Code,"), Some("r#type"));
assert_eq!(parse_field_name("pub struct HumanName {"), None);
assert_eq!(parse_field_name(" /// a doc comment"), None);
}
#[test]
fn struct_prefix_map_has_known_entries() {
let map = struct_path_prefixes();
assert_eq!(map.get("HumanName").map(String::as_str), Some("HumanName"));
assert_eq!(map.get("PatientContact").map(String::as_str), Some("Patient.contact"));
}
#[test]
#[ignore = "dry-run report only"]
fn report_types() {
let (plan, skipped) = plan_group("types");
println!("types: {} sibling insertions planned", plan.len());
for ins in &plan {
println!(
" {} :: {} -> {} ({})",
ins.struct_name,
ins.field,
ins.json_key,
if ins.repeating { "repeating" } else { "scalar" }
);
}
let mut choice_like: Vec<_> = skipped
.iter()
.filter(|(_, f)| f.contains('_') && f.rsplit('_').next().is_some())
.collect();
choice_like.sort();
println!("skipped (no meta path; incl. choice value[x]): {}", skipped.len());
}
#[test]
#[ignore = "writes src/r5/types/*.rs"]
fn apply_types() {
let n = apply_group("types");
println!("types: applied {n} sibling insertions");
}
#[test]
#[ignore = "writes src/r5/resources/*.rs"]
fn apply_resources() {
let n = apply_group("resources");
println!("resources: applied {n} sibling insertions");
}
#[test]
#[ignore = "writes src/r5/types/*.rs"]
fn apply_element_base() {
let n = apply_element_base_group();
println!("types: added Element base (id + extension) to {n} datatypes");
}
}