use std::collections::BTreeMap;
use std::fmt::Write as _;
use super::naming;
use super::spec::StructureDefinition;
use super::version::Version;
#[derive(Debug, Clone, Copy)]
pub struct Primitive {
pub code: &'static str,
pub inner: &'static str,
pub inner_doc: &'static str,
pub default_value: &'static str,
pub string_encoded: bool,
pub sample: &'static str,
pub sample_json: &'static str,
}
const DEFAULT_STRING: &str = "std::string::String::new()";
const PRIMITIVES: &[Primitive] = &[
Primitive { code: "base64Binary", inner: "std::string::String", inner_doc: "The base64-encoded string representation of the binary content.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"YWJj\".to_string()", sample_json: "\"YWJj\"" },
Primitive { code: "boolean", inner: "bool", inner_doc: "The underlying `true` or `false` value.", default_value: "false", string_encoded: false, sample: "true", sample_json: "true" },
Primitive { code: "canonical", inner: "std::string::String", inner_doc: "The canonical URL, optionally with a `|version` suffix.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"http://example.org/vs\".to_string()", sample_json: "\"http://example.org/vs\"" },
Primitive { code: "code", inner: "std::string::String", inner_doc: "The code as it appears on the wire.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"final\".to_string()", sample_json: "\"final\"" },
Primitive { code: "date", inner: "std::string::String", inner_doc: "The date, at whatever precision it was written.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"2019-11-01\".to_string()", sample_json: "\"2019-11-01\"" },
Primitive { code: "dateTime", inner: "std::string::String", inner_doc: "The date and time, at whatever precision it was written.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"2019-11-01T09:29:23Z\".to_string()", sample_json: "\"2019-11-01T09:29:23Z\"" },
Primitive { code: "decimal", inner: "::serde_json::Number", inner_doc: "The numeric value, preserved exactly as written so that significant trailing zeros survive a round trip.", default_value: "::serde_json::Number::from(0)", string_encoded: false, sample: "::serde_json::Number::from(42)", sample_json: "42" },
Primitive { code: "id", inner: "std::string::String", inner_doc: "The logical id: up to 64 characters of `[A-Za-z0-9-.]`.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"pat-1\".to_string()", sample_json: "\"pat-1\"" },
Primitive { code: "instant", inner: "std::string::String", inner_doc: "The instant, always to at least second precision with a timezone.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"2019-11-01T09:29:23Z\".to_string()", sample_json: "\"2019-11-01T09:29:23Z\"" },
Primitive { code: "integer", inner: "i32", inner_doc: "The signed 32-bit value.", default_value: "0", string_encoded: false, sample: "42", sample_json: "42" },
Primitive { code: "integer64", inner: "i64", inner_doc: "The signed 64-bit value.", default_value: "0", string_encoded: true, sample: "9_000_000_000", sample_json: "\"9000000000\"" },
Primitive { code: "markdown", inner: "std::string::String", inner_doc: "The Markdown source text.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"# Heading\".to_string()", sample_json: "\"# Heading\"" },
Primitive { code: "oid", inner: "std::string::String", inner_doc: "The OID as a `urn:oid:` URI.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"urn:oid:1.2.3\".to_string()", sample_json: "\"urn:oid:1.2.3\"" },
Primitive { code: "positiveInt", inner: "u32", inner_doc: "The value, which the specification constrains to be greater than zero.", default_value: "0", string_encoded: false, sample: "1", sample_json: "1" },
Primitive { code: "string", inner: "std::string::String", inner_doc: "The underlying Unicode string value.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"abc\".to_string()", sample_json: "\"abc\"" },
Primitive { code: "time", inner: "std::string::String", inner_doc: "The time of day, without a date or timezone.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"09:29:23\".to_string()", sample_json: "\"09:29:23\"" },
Primitive { code: "unsignedInt", inner: "u32", inner_doc: "The value, which the specification constrains to be zero or greater.", default_value: "0", string_encoded: false, sample: "0", sample_json: "0" },
Primitive { code: "uri", inner: "std::string::String", inner_doc: "The URI, which may be absolute or relative.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"http://example.org\".to_string()", sample_json: "\"http://example.org\"" },
Primitive { code: "url", inner: "std::string::String", inner_doc: "The URL.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"http://example.org\".to_string()", sample_json: "\"http://example.org\"" },
Primitive { code: "uuid", inner: "std::string::String", inner_doc: "The UUID as a `urn:uuid:` URI.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"urn:uuid:c757873d-ec9a-4326-a141-556f43239520\".to_string()", sample_json: "\"urn:uuid:c757873d-ec9a-4326-a141-556f43239520\"" },
Primitive { code: "xhtml", inner: "std::string::String", inner_doc: "The XHTML fragment, which must be a well-formed `<div>`.", default_value: DEFAULT_STRING, string_encoded: false, sample: "\"<div xmlns=\\\"http://www.w3.org/1999/xhtml\\\"/>\".to_string()", sample_json: "\"<div xmlns=\\\"http://www.w3.org/1999/xhtml\\\"/>\"" },
];
impl Primitive {
#[must_use]
pub fn needs_default_impl(&self) -> bool {
self.inner == "::serde_json::Number"
}
}
#[must_use]
pub fn lookup(code: &str) -> Option<&'static Primitive> {
PRIMITIVES.iter().find(|p| p.code == code)
}
#[must_use]
pub fn render(sd: &StructureDefinition, version: Version) -> Option<String> {
let primitive = lookup(&sd.type_name)?;
let name = naming::pascal(&sd.type_name);
let module = version.module();
let mut out = format!("//! {}\n//!\n", sd.type_name);
let _ = write!(out, "//! URL: {}\n//!\n", sd.url);
let _ = write!(out, "//! Version: {}\n//!\n", sd.version.clone().unwrap_or_default());
let _ = write!(out, "//! FHIR {}: <{}>\n\n", version.label(), version.spec_url());
out.push_str("use ::serde::{Deserialize, Serialize};\n");
if primitive.string_encoded {
out.push_str("use ::serde_with::{serde_as, DisplayFromStr};\n");
}
out.push('\n');
out.push_str(&naming::doc_comment(sd.description.as_deref().unwrap_or_default(), ""));
let _ = write!(
out,
"///\n\
/// # Examples\n\
///\n\
/// ```\n\
/// use fhir::{module}::types::{}::{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",
naming::module_name(&sd.type_name),
);
if primitive.string_encoded {
out.push_str("#[serde_as]\n");
}
let derive_default = if primitive.needs_default_impl() { "" } else { "Default, " };
let _ = writeln!(
out,
"#[derive(Debug, {derive_default}Clone, Serialize, Deserialize, PartialEq, Eq)]"
);
let _ = writeln!(out, "pub struct {name}(");
out.push_str(&naming::doc_comment(primitive.inner_doc, " "));
if primitive.string_encoded {
out.push_str(" // FHIR carries this as a JSON string so that it survives\n");
out.push_str(" // consumers whose numbers are 64-bit floats.\n");
out.push_str(" #[serde_as(as = \"DisplayFromStr\")]\n");
}
let _ = writeln!(out, " pub {},", primitive.inner);
out.push_str(");\n");
if primitive.needs_default_impl() {
let _ = write!(
out,
"\nimpl Default for {name} {{\n fn default() -> Self {{\n {name}({})\n }}\n}}\n",
primitive.default_value,
);
}
let default_expr = format!("{name}({})", primitive.default_value);
let _ = write!(
out,
"\n#[cfg(test)]\n\
mod tests {{\n\
\x20 use super::*;\n\
\x20 use ::serde_json::json;\n\
\n\
\x20 #[test]\n\
\x20 fn test_default() {{\n\
\x20 assert_eq!({name}::default(), {default_expr});\n\
\x20 }}\n\
\n\
\x20 #[test]\n\
\x20 fn test_serde() {{\n\
\x20 let value = {name}({});\n\
\x20 let json = ::serde_json::to_value(&value).expect(\"to_value\");\n\
\x20 assert_eq!(json, json!({}));\n\
\x20 let back: {name} = ::serde_json::from_value(json).expect(\"from_value\");\n\
\x20 assert_eq!(value, back);\n\
\x20 }}\n\
}}\n",
primitive.sample, primitive.sample_json,
);
Some(out)
}
#[must_use]
pub fn codes_of(definitions: &[StructureDefinition]) -> BTreeMap<String, StructureDefinition> {
definitions
.iter()
.filter(|sd| sd.kind == "primitive-type")
.map(|sd| (sd.type_name.clone(), sd.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn sd(name: &str) -> StructureDefinition {
::serde_json::from_value(::serde_json::json!({
"name": name, "type": name, "kind": "primitive-type",
"url": format!("http://hl7.org/fhir/StructureDefinition/{name}"),
"version": "4.0.1",
"description": "A primitive.",
"snapshot": { "element": [{ "path": name }] }
}))
.unwrap()
}
#[test]
fn every_table_entry_has_a_unique_code() {
let mut codes: Vec<&str> = PRIMITIVES.iter().map(|p| p.code).collect();
let count = codes.len();
codes.sort_unstable();
codes.dedup();
assert_eq!(codes.len(), count);
}
#[test]
fn string_primitive_is_a_transparent_newtype() {
let out = render(&sd("string"), Version::R4).unwrap();
assert!(out.contains("pub struct String("));
assert!(out.contains("pub std::string::String,"));
assert!(out.contains("String(std::string::String::new())"));
assert!(out.contains("#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]"));
assert!(out.contains("use fhir::r4::types::string::String;"));
}
#[test]
fn decimal_gets_a_hand_written_default() {
let out = render(&sd("decimal"), Version::R4).unwrap();
assert!(out.contains("pub ::serde_json::Number,"));
assert!(out.contains("impl Default for Decimal"));
assert!(out.contains("Decimal(::serde_json::Number::from(0))"));
assert!(out.contains("#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]"));
}
#[test]
fn integer64_is_carried_as_a_json_string() {
let out = render(&sd("integer64"), Version::R5).unwrap();
assert!(out.contains("#[serde_as]"));
assert!(out.contains("#[serde_as(as = \"DisplayFromStr\")]"));
assert!(out.contains("pub i64,"));
}
#[test]
fn an_unknown_primitive_is_reported_rather_than_guessed() {
assert!(render(&sd("quantum"), Version::R4).is_none());
}
}