use crate::json::{self, Value};
use std::collections::BTreeMap;
use std::fmt;
pub const VARIABLE: &str = "VAR";
const UNSTATED: &str = "";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Item {
Segment {
name: String,
required: bool,
repeats: bool,
},
Group {
name: String,
required: bool,
repeats: bool,
items: Vec<Item>,
},
}
impl Item {
#[must_use]
pub fn name(&self) -> &str {
match self {
Item::Segment { name, .. } | Item::Group { name, .. } => name,
}
}
#[must_use]
pub fn required(&self) -> bool {
match self {
Item::Segment { required, .. } | Item::Group { required, .. } => *required,
}
}
#[must_use]
pub fn repeats(&self) -> bool {
match self {
Item::Segment { repeats, .. } | Item::Group { repeats, .. } => *repeats,
}
}
#[must_use]
pub fn can_start(&self, segment: &str) -> bool {
match self {
Item::Segment { name, .. } => name == segment,
Item::Group { items, .. } => {
for item in items {
if item.can_start(segment) {
return true;
}
if item.required() {
return false;
}
}
false
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Dictionary {
name: String,
version: Option<String>,
types: BTreeMap<String, Vec<String>>,
segments: BTreeMap<String, Vec<String>>,
cardinality: BTreeMap<String, Vec<Cardinality>>,
structures: BTreeMap<String, Vec<Item>>,
aliases: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Cardinality {
pub required: bool,
pub repeats: bool,
}
impl Dictionary {
pub fn empty(name: impl Into<String>) -> Dictionary {
Dictionary {
name: name.into(),
..Dictionary::default()
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn version(&self) -> Option<&str> {
self.version.as_deref()
}
pub fn composite_components(&self, data_type: &str) -> Option<&[String]> {
self.types.get(data_type).map(Vec::as_slice)
}
#[must_use]
pub fn is_composite(&self, data_type: &str) -> bool {
self.types.contains_key(data_type)
}
pub fn segment_fields(&self, segment: &str) -> Option<&[String]> {
self.segments.get(segment).map(Vec::as_slice)
}
pub fn field_type(&self, segment: &str, field: usize) -> Option<&str> {
let types = self.segment_fields(segment)?;
match types.get(field.checked_sub(1)?).map(String::as_str) {
Some(UNSTATED) | None => None,
found => found,
}
}
#[must_use]
pub fn field_cardinality(&self, segment: &str, field: usize) -> Cardinality {
field
.checked_sub(1)
.and_then(|index| self.cardinality.get(segment)?.get(index).copied())
.unwrap_or_default()
}
#[must_use]
pub fn variable_type(&self, segment: &er7::Segment) -> Option<&str> {
let named = segment
.component(2, 1)?
.subcomponent(1)?
.raw
.trim()
.to_string();
self.types
.get_key_value(&named)
.map(|(key, _)| key.as_str())
}
pub fn structure(&self, id: &str) -> Option<&[Item]> {
self.structures.get(id).map(Vec::as_slice)
}
#[must_use]
pub fn structure_id(&self, code: &str, trigger: &str) -> String {
if code.is_empty() {
return "HL7Message".to_string();
}
let joined = if trigger.is_empty() {
code.to_string()
} else {
format!("{code}_{trigger}")
};
if let Some(target) = self.aliases.get(&joined) {
return target.clone();
}
if self.structures.contains_key(&joined) {
return joined;
}
if self.structures.contains_key(code) {
return code.to_string();
}
joined
}
pub fn structure_ids(&self) -> impl Iterator<Item = &str> {
self.structures.keys().map(String::as_str)
}
pub fn segment_names(&self) -> impl Iterator<Item = &str> {
self.segments.keys().map(String::as_str)
}
pub fn type_names(&self) -> impl Iterator<Item = &str> {
self.types.keys().map(String::as_str)
}
pub fn from_json(text: &str, name: impl Into<String>) -> Result<Dictionary, Error> {
Dictionary::from_json_resolving(text, name, |version| {
crate::Version::parse(version).map(crate::Version::dictionary)
})
}
pub fn from_json_over(
text: &str,
name: impl Into<String>,
base: &Dictionary,
) -> Result<Dictionary, Error> {
let name = name.into();
let value = json::parse(text).map_err(Error::Json)?;
let mut dictionary = base.clone();
dictionary.name = name;
dictionary.version = None;
dictionary.apply(&value)?;
Ok(dictionary)
}
pub fn from_json_resolving(
text: &str,
name: impl Into<String>,
resolve: impl Fn(&str) -> Option<std::sync::Arc<Dictionary>>,
) -> Result<Dictionary, Error> {
let name = name.into();
let value = json::parse(text).map_err(Error::Json)?;
let mut dictionary = match value.get("inherits") {
None => Dictionary::empty(name.clone()),
Some(Value::String(base)) => match resolve(base) {
Some(base) => Dictionary {
name: name.clone(),
..(*base).clone()
},
None => return Err(Error::UnknownBase(base.clone())),
},
Some(other) => {
return Err(Error::field("inherits", "a version string", other));
}
};
dictionary.apply(&value)?;
Ok(dictionary)
}
fn apply(&mut self, value: &Value) -> Result<(), Error> {
if value.as_object().is_none() {
return Err(Error::field("<document>", "an object", value));
}
if let Some(version) = value.get("version") {
match version.as_str() {
Some(text) => self.version = Some(text.to_string()),
None => return Err(Error::field("version", "a version string", version)),
}
}
for section in ["types", "segments"] {
let Some(members) = value.get(section) else {
continue;
};
let members = members
.as_object()
.ok_or_else(|| Error::field(section, "an object", members))?;
for (key, entry) in members {
let is_segments = section == "segments";
let table = if is_segments {
&mut self.segments
} else {
&mut self.types
};
if entry.is_null() {
table.remove(key);
if is_segments {
self.cardinality.remove(key);
}
continue;
}
let inherited = table.get(key).cloned().unwrap_or_default();
let inherited_cardinality = if is_segments {
self.cardinality.get(key).cloned().unwrap_or_default()
} else {
Vec::new()
};
let (names, cardinality) = positions(
entry,
inherited,
inherited_cardinality,
&format!("{section}.{key}"),
)?;
table.insert(key.clone(), names);
if is_segments {
self.cardinality.insert(key.clone(), cardinality);
}
}
}
if let Some(aliases) = value.get("aliases") {
let members = aliases
.as_object()
.ok_or_else(|| Error::field("aliases", "an object", aliases))?;
for (key, entry) in members {
if entry.is_null() {
self.aliases.remove(key);
continue;
}
let target = entry.as_str().ok_or_else(|| {
Error::field(&format!("aliases.{key}"), "a structure ID", entry)
})?;
self.aliases.insert(key.clone(), target.to_string());
}
}
if let Some(structures) = value.get("structures") {
let members = structures
.as_object()
.ok_or_else(|| Error::field("structures", "an object", structures))?;
for (key, entry) in members {
if entry.is_null() {
self.structures.remove(key);
continue;
}
let items = parse_items(entry, &format!("structures.{key}"))?;
self.structures.insert(key.clone(), items);
}
}
Ok(())
}
}
fn positions(
value: &Value,
inherited: Vec<String>,
inherited_cardinality: Vec<Cardinality>,
path: &str,
) -> Result<(Vec<String>, Vec<Cardinality>), Error> {
if let Some(list) = value.as_array() {
let mut names = Vec::with_capacity(list.len());
let mut cardinality = Vec::with_capacity(list.len());
for (index, item) in list.iter().enumerate() {
let (name, card) = entry_of(item, &format!("{path}[{index}]"))?;
names.push(name);
cardinality.push(card);
}
return Ok((names, cardinality));
}
let members = value
.as_object()
.ok_or_else(|| Error::field(path, "an array, or an object of position overrides", value))?;
let mut names = inherited;
let mut cardinality = inherited_cardinality;
for (key, entry) in members {
let path = format!("{path}.{key}");
let position: usize = key
.parse()
.ok()
.filter(|position| *position > 0)
.ok_or_else(|| Error::Field {
path: path.clone(),
expected: "a 1-based position number".to_string(),
found: format!("{key:?}"),
})?;
let (name, card) = entry_of(entry, &path)?;
if names.len() < position {
names.resize(position, UNSTATED.to_string());
}
if cardinality.len() < position {
cardinality.resize(position, Cardinality::default());
}
names[position - 1] = name;
cardinality[position - 1] = card;
}
cardinality.resize(names.len(), Cardinality::default());
Ok((names, cardinality))
}
fn entry_of(value: &Value, path: &str) -> Result<(String, Cardinality), Error> {
if let Some(name) = value.as_str() {
return Ok((name.to_string(), Cardinality::default()));
}
if value.as_object().is_none() {
return Err(Error::field(path, "a data type name", value));
}
let name = value
.get("type")
.ok_or_else(|| Error::missing(&format!("{path}.type")))?
.as_str()
.ok_or_else(|| {
Error::field(
&format!("{path}.type"),
"a data type name",
value.get("type").unwrap_or(value),
)
})?;
Ok((
name.to_string(),
Cardinality {
required: flag(value, "required", path)?,
repeats: flag(value, "repeats", path)?,
},
))
}
fn parse_items(value: &Value, path: &str) -> Result<Vec<Item>, Error> {
let list = value
.as_array()
.ok_or_else(|| Error::field(path, "an array of structure items", value))?;
let mut items = Vec::with_capacity(list.len());
for (index, entry) in list.iter().enumerate() {
let path = format!("{path}[{index}]");
if let Some(name) = entry.as_str() {
items.push(Item::Segment {
name: name.to_string(),
required: false,
repeats: false,
});
continue;
}
let required = flag(entry, "required", &path)?;
let repeats = flag(entry, "repeats", &path)?;
if let Some(name) = entry.get("segment") {
let name = name
.as_str()
.ok_or_else(|| Error::field(&format!("{path}.segment"), "a segment name", name))?;
items.push(Item::Segment {
name: name.to_string(),
required,
repeats,
});
} else if let Some(name) = entry.get("group") {
let name = name
.as_str()
.ok_or_else(|| Error::field(&format!("{path}.group"), "a group name", name))?;
let children = entry
.get("items")
.ok_or_else(|| Error::missing(&format!("{path}.items")))?;
items.push(Item::Group {
name: name.to_string(),
required,
repeats,
items: parse_items(children, &format!("{path}.items"))?,
});
} else {
return Err(Error::field(
&path,
"an item with a `segment` or `group` member",
entry,
));
}
}
Ok(items)
}
fn flag(entry: &Value, name: &str, path: &str) -> Result<bool, Error> {
match entry.get(name) {
None => Ok(false),
Some(value) => value
.as_bool()
.ok_or_else(|| Error::field(&format!("{path}.{name}"), "true or false", value)),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
Json(json::Error),
Field {
path: String,
expected: String,
found: String,
},
Missing {
path: String,
},
UnknownBase(String),
}
impl Error {
fn field(path: &str, expected: &str, found: &Value) -> Error {
Error::Field {
path: path.to_string(),
expected: expected.to_string(),
found: found.kind().to_string(),
}
}
fn missing(path: &str) -> Error {
Error::Missing {
path: path.to_string(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Json(error) => write!(f, "{error}"),
Error::Field {
path,
expected,
found,
} => write!(f, "{path}: expected {expected}, found {found}"),
Error::Missing { path } => write!(f, "{path}: required member is missing"),
Error::UnknownBase(base) => {
write!(f, "`inherits`: {base:?} is not a known HL7 version")
}
}
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
use crate::Version;
#[test]
fn reads_the_base_release() {
let dictionary = Version::V2_5.dictionary();
assert_eq!(dictionary.field_type("PID", 5), Some("XPN"));
assert_eq!(dictionary.field_type("MSH", 9), Some("MSG"));
assert_eq!(dictionary.field_type("OBX", 5), Some(VARIABLE));
assert_eq!(dictionary.field_type("PID", 999), None);
assert_eq!(dictionary.field_type("ZZZ", 1), None);
assert_eq!(
dictionary
.composite_components("XPN")
.map(|c| c[0].as_str()),
Some("FN")
);
assert!(!dictionary.is_composite("ST"));
assert!(dictionary.structure("ORU_R01").is_some());
}
#[test]
fn a_delta_adds_removes_and_inherits() {
let dictionary = Dictionary::from_json(
r#"{
"inherits": "2.5",
"types": { "TS": ["ST"], "XPN": null },
"segments": { "ZPD": ["ST", "CX"] },
"structures": { "ORU_R01": null }
}"#,
"test",
)
.unwrap();
assert_eq!(dictionary.composite_components("TS").unwrap(), ["ST"]); assert_eq!(dictionary.composite_components("XPN"), None); assert_eq!(dictionary.field_type("ZPD", 2), Some("CX")); assert_eq!(dictionary.field_type("PID", 5), Some("XPN")); assert_eq!(dictionary.structure("ORU_R01"), None); assert!(dictionary.structure("ACK").is_some()); assert_eq!(dictionary.name(), "test");
}
#[test]
fn a_sparse_delta_restates_one_position_and_keeps_the_rest() {
let dictionary = Dictionary::from_json(
r#"{"inherits": "2.5", "segments": {"MSH": {"12": "ID"}}}"#,
"test",
)
.unwrap();
assert_eq!(dictionary.field_type("MSH", 12), Some("ID")); assert_eq!(dictionary.field_type("MSH", 9), Some("MSG")); assert_eq!(dictionary.field_type("MSH", 21), Some("EI")); let dictionary =
Dictionary::from_json(r#"{"segments": {"ZZZ": {"3": "CX"}}}"#, "test").unwrap();
assert_eq!(dictionary.field_type("ZZZ", 3), Some("CX"));
assert_eq!(dictionary.field_type("ZZZ", 1), None);
let error =
Dictionary::from_json(r#"{"segments": {"ZZZ": {"0": "CX"}}}"#, "test").unwrap_err();
assert!(error.to_string().contains("1-based position"), "{error}");
}
#[test]
fn reads_structures_including_the_string_shorthand() {
let dictionary = Dictionary::from_json(
r#"{"structures": {"ZZZ_Z01": [
{"segment": "MSH", "required": true},
"NTE",
{"group": "ORDER", "repeats": true, "items": [{"segment": "ORC", "required": true}]}
]}}"#,
"test",
)
.unwrap();
let items = dictionary.structure("ZZZ_Z01").unwrap();
assert!(matches!(&items[0], Item::Segment { name, required: true, .. } if name == "MSH"));
assert!(matches!(
&items[1],
Item::Segment {
required: false,
repeats: false,
..
}
));
assert!(items[2].repeats() && !items[2].required());
assert!(items[2].can_start("ORC"));
assert!(!items[2].can_start("OBX"));
}
#[test]
fn a_group_can_start_at_any_leading_optional_segment() {
let dictionary = Version::V2_5.dictionary();
let items = dictionary.structure("ORU_R01").unwrap();
let patient_result = &items[2];
assert_eq!(patient_result.name(), "PATIENT_RESULT");
assert!(patient_result.can_start("PID"));
assert!(patient_result.can_start("OBR"));
assert!(!patient_result.can_start("MSA"));
}
#[test]
fn resolves_obx_5_through_obx_2() {
let dictionary = Version::V2_5.dictionary();
let message = er7::parse("MSH|^~\\&|A||||1||ORU^R01|1|P|2.5\rOBX|1|CE|X||a^b").unwrap();
let obx = message.segment("OBX").unwrap();
assert_eq!(dictionary.variable_type(obx), Some("CE"));
let message = er7::parse("MSH|^~\\&|A||||1||ORU^R01|1|P|2.5\rOBX|1|NM|X||7").unwrap();
assert_eq!(
dictionary.variable_type(message.segment("OBX").unwrap()),
None
);
}
#[test]
fn a_field_may_state_its_cardinality_as_well_as_its_type() {
let dictionary = Dictionary::from_json(
r#"{"segments": {"PID": [
"SI",
{"type": "CX", "required": true},
{"type": "XTN", "repeats": true},
{"type": "ST", "required": true, "repeats": true}
]}}"#,
"x",
)
.unwrap();
assert_eq!(dictionary.field_type("PID", 1), Some("SI"));
assert_eq!(dictionary.field_type("PID", 2), Some("CX"));
assert_eq!(
dictionary.field_cardinality("PID", 1),
Cardinality::default()
);
assert_eq!(
dictionary.field_cardinality("PID", 2),
Cardinality {
required: true,
repeats: false
}
);
assert_eq!(
dictionary.field_cardinality("PID", 3),
Cardinality {
required: false,
repeats: true
}
);
assert_eq!(
dictionary.field_cardinality("PID", 4),
Cardinality {
required: true,
repeats: true
}
);
assert_eq!(
dictionary.field_cardinality("PID", 99),
Cardinality::default()
);
assert_eq!(
dictionary.field_cardinality("ZZZ", 1),
Cardinality::default()
);
assert_eq!(
dictionary.field_cardinality("PID", 0),
Cardinality::default()
);
}
#[test]
fn cardinality_layers_and_is_removed_like_everything_else() {
let dictionary = Dictionary::from_json(
r#"{"inherits": "2.5", "segments": {"PID": {"13": {"type": "XTN", "repeats": true}}}}"#,
"x",
)
.unwrap();
assert!(dictionary.field_cardinality("PID", 13).repeats);
assert!(!dictionary.field_cardinality("PID", 5).repeats);
assert_eq!(dictionary.field_type("PID", 5), Some("XPN"));
let dictionary =
Dictionary::from_json(r#"{"inherits": "2.5", "segments": {"PID": null}}"#, "x")
.unwrap();
assert_eq!(
dictionary.field_cardinality("PID", 13),
Cardinality::default()
);
}
#[test]
fn reports_where_a_malformed_dictionary_is_wrong() {
let error = Dictionary::from_json(r#"{"segments": {"PID": [1]}}"#, "x").unwrap_err();
assert_eq!(
error.to_string(),
"segments.PID[0]: expected a data type name, found number"
);
let error = Dictionary::from_json(r#"{"inherits": "9.9"}"#, "x").unwrap_err();
assert!(matches!(error, Error::UnknownBase(_)), "{error}");
let error =
Dictionary::from_json(r#"{"structures": {"A": [{"group": "G"}]}}"#, "x").unwrap_err();
assert_eq!(
error.to_string(),
"structures.A[0].items: required member is missing"
);
assert!(matches!(
Dictionary::from_json("not json", "x"),
Err(Error::Json(_))
));
}
#[test]
fn layering_over_an_explicit_base_ignores_inherits() {
let base = Dictionary::from_json(r#"{"segments": {"AAA": ["ST"]}}"#, "base").unwrap();
let over = Dictionary::from_json_over(
r#"{"inherits": "2.5", "segments": {"BBB": ["NM"]}}"#,
"over",
&base,
)
.unwrap();
assert_eq!(over.field_type("AAA", 1), Some("ST"));
assert_eq!(over.field_type("BBB", 1), Some("NM"));
assert_eq!(
over.field_type("PID", 5),
None,
"2.5 must not have been pulled in"
);
}
}