use crate::error::ParseError;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TermDefinition {
text: String,
description: Option<String>,
}
impl TermDefinition {
pub fn new(
text: impl Into<String>,
description: Option<String>,
) -> Result<Self, ParseError> {
let text = text.into();
if text.trim().is_empty() {
return Err(ParseError::invariant("ARCHETYPE_TERM", "empty text"));
}
Ok(Self { text, description })
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArchetypeTerminology {
original_language: String,
term_definitions: BTreeMap<String, BTreeMap<String, TermDefinition>>,
value_sets: BTreeMap<String, BTreeSet<String>>,
term_bindings: BTreeMap<String, BTreeMap<String, String>>,
}
impl ArchetypeTerminology {
pub fn new(
original_language: impl Into<String>,
definitions: BTreeMap<String, TermDefinition>,
) -> Result<Self, ParseError> {
let original_language = original_language.into();
if original_language.trim().is_empty() {
return Err(ParseError::invariant(
"ARCHETYPE_TERMINOLOGY",
"empty original_language",
));
}
if definitions.is_empty() {
return Err(ParseError::invariant(
"ARCHETYPE_TERMINOLOGY",
"no term definitions for the original language",
));
}
let mut term_definitions = BTreeMap::new();
term_definitions.insert(original_language.clone(), definitions);
Ok(Self {
original_language,
term_definitions,
value_sets: BTreeMap::new(),
term_bindings: BTreeMap::new(),
})
}
pub fn with_value_set(
mut self,
ac_code: impl Into<String>,
members: BTreeSet<String>,
) -> Result<Self, ParseError> {
let ac_code = ac_code.into();
if members.is_empty() {
return Err(ParseError::invariant("ARCHETYPE_TERMINOLOGY", "VACDF"));
}
for member in &members {
if !self.defines(member) {
return Err(ParseError::new("ARCHETYPE_TERMINOLOGY", "VATDF", member));
}
}
self.value_sets.insert(ac_code, members);
Ok(self)
}
#[must_use]
pub fn with_binding(
mut self,
terminology: impl Into<String>,
code: impl Into<String>,
external: impl Into<String>,
) -> Self {
self.term_bindings
.entry(terminology.into())
.or_default()
.insert(code.into(), external.into());
self
}
#[must_use]
pub fn original_language(&self) -> &str {
&self.original_language
}
#[must_use]
pub fn defines(&self, code: &str) -> bool {
self.term_definitions
.get(&self.original_language)
.is_some_and(|definitions| definitions.contains_key(code))
}
#[must_use]
pub fn definition(&self, code: &str) -> Option<&TermDefinition> {
self.term_definitions
.get(&self.original_language)?
.get(code)
}
#[must_use]
pub fn value_set(&self, ac_code: &str) -> Option<&BTreeSet<String>> {
self.value_sets.get(ac_code)
}
pub fn codes(&self) -> impl Iterator<Item = &str> {
self.term_definitions
.get(&self.original_language)
.into_iter()
.flat_map(|definitions| definitions.keys().map(String::as_str))
}
#[must_use]
pub const fn bindings(&self) -> &BTreeMap<String, BTreeMap<String, String>> {
&self.term_bindings
}
}
#[cfg(test)]
mod tests {
use super::*;
fn definitions(codes: &[(&str, &str)]) -> BTreeMap<String, TermDefinition> {
codes
.iter()
.map(|(code, text)| {
(
(*code).to_owned(),
TermDefinition::new(*text, None).unwrap(),
)
})
.collect()
}
#[test]
fn a_value_set_member_nobody_defined_is_refused() {
let terminology =
ArchetypeTerminology::new("en", definitions(&[("at0001", "Systolic")])).unwrap();
let err = terminology
.clone()
.with_value_set("ac0001", BTreeSet::from(["at0009".to_owned()]))
.unwrap_err();
assert_eq!(err.reason, "VATDF");
assert!(
terminology
.with_value_set("ac0001", BTreeSet::from(["at0001".to_owned()]))
.is_ok()
);
}
#[test]
fn an_empty_rubric_is_refused() {
assert!(TermDefinition::new(" ", None).is_err());
}
#[test]
fn external_bindings_are_carried_and_not_checked() {
let terminology = ArchetypeTerminology::new("en", definitions(&[("at0001", "Systolic")]))
.unwrap()
.with_binding("SNOMED-CT", "at0001", "271649006");
assert_eq!(terminology.bindings()["SNOMED-CT"]["at0001"], "271649006");
assert!(terminology.defines("at0001"));
}
}