use crate::concept::{Concept, Designation, Match, Property};
use crate::error::{Outcome, UpstreamError};
#[derive(Debug)]
#[non_exhaustive]
pub enum LookupOutcome {
Found {
name: String,
version: Option<String>,
display: String,
designations: Vec<Designation>,
properties: Vec<Property>,
},
NotFound {
outcome: Box<UpstreamError>,
},
}
#[derive(Debug)]
#[non_exhaustive]
pub enum TranslateOutcome {
Translated {
matches: Vec<Match>,
message: Option<String>,
},
NoMatch {
message: Option<String>,
},
}
impl TranslateOutcome {
pub fn accepted(&self) -> impl Iterator<Item = &Match> {
let matches = match self {
Self::Translated { matches, .. } => matches.as_slice(),
Self::NoMatch { .. } => &[],
};
matches.iter().filter(|found| found.accepted())
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ValidateOutcome {
Valid {
display: Option<String>,
message: Option<String>,
},
Invalid {
message: Option<String>,
outcome: Option<Outcome>,
},
}
impl ValidateOutcome {
#[must_use]
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Request {
Lookup {
system: String,
code: String,
version: Option<String>,
date: Option<String>,
},
Translate(Translation),
ValidateCode(Validation),
Expand(Expansion),
Subsumes(Subsumption),
}
impl Request {
#[must_use]
pub const fn operation(&self) -> &'static str {
match self {
Self::Lookup { .. } => crate::LOOKUP,
Self::Translate(_) => crate::TRANSLATE,
Self::ValidateCode(_) => crate::VALIDATE_CODE,
Self::Expand(_) => crate::EXPAND,
Self::Subsumes(_) => crate::SUBSUMES,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Translation {
pub system: String,
pub code: String,
pub version: Option<String>,
pub concept_map_url: Option<String>,
pub target_system: Option<String>,
pub reverse: Option<bool>,
}
impl Translation {
#[must_use]
pub fn new(system: impl Into<String>, code: impl Into<String>) -> Self {
Self {
system: system.into(),
code: code.into(),
version: None,
concept_map_url: None,
target_system: None,
reverse: None,
}
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
#[must_use]
pub fn with_concept_map(mut self, url: impl Into<String>) -> Self {
self.concept_map_url = Some(url.into());
self
}
#[must_use]
pub fn with_target_system(mut self, system: impl Into<String>) -> Self {
self.target_system = Some(system.into());
self
}
#[must_use]
pub const fn with_reverse(mut self, reverse: bool) -> Self {
self.reverse = Some(reverse);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Validation {
pub value_set_url: String,
pub value_set_version: Option<String>,
pub date: Option<String>,
pub coded: Coded,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Coded {
Code {
system: String,
code: String,
version: Option<String>,
display: Option<String>,
},
Coding(Concept),
CodeableConcept {
coding: Vec<Concept>,
text: Option<String>,
},
}
#[derive(Debug)]
#[non_exhaustive]
pub enum BatchOutcome {
Lookup(LookupOutcome),
Translate(TranslateOutcome),
ValidateCode(ValidateOutcome),
Expand(ExpandOutcome),
Subsumes(SubsumesOutcome),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Expansion {
pub value_set_url: String,
pub value_set_version: Option<String>,
pub date: Option<String>,
pub filter: Option<String>,
pub offset: Option<u32>,
pub count: Option<u32>,
pub include_designations: Option<bool>,
}
impl Expansion {
#[must_use]
pub fn new(value_set_url: impl Into<String>) -> Self {
Self {
value_set_url: value_set_url.into(),
value_set_version: None,
date: None,
filter: None,
offset: None,
count: None,
include_designations: None,
}
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.value_set_version = Some(version.into());
self
}
#[must_use]
pub fn with_date(mut self, date: impl Into<String>) -> Self {
self.date = Some(date.into());
self
}
#[must_use]
pub fn with_filter(mut self, filter: impl Into<String>) -> Self {
self.filter = Some(filter.into());
self
}
#[must_use]
pub const fn with_page(mut self, offset: u32, count: u32) -> Self {
self.offset = Some(offset);
self.count = Some(count);
self
}
#[must_use]
pub const fn with_designations(mut self, include: bool) -> Self {
self.include_designations = Some(include);
self
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ExpandOutcome {
Expanded(Expanded),
NotFound {
outcome: Box<UpstreamError>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Expanded {
pub url: Option<String>,
pub version: Option<String>,
pub total: Option<i32>,
pub offset: Option<i32>,
pub contains: Vec<ExpansionContains>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExpansionContains {
pub system: Option<String>,
pub version: Option<String>,
pub code: Option<String>,
pub display: Option<String>,
pub r#abstract: Option<bool>,
pub inactive: Option<bool>,
pub designations: Vec<Designation>,
pub contains: Vec<ExpansionContains>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Subsumption {
pub system: String,
pub code_a: String,
pub code_b: String,
pub version: Option<String>,
}
impl Subsumption {
#[must_use]
pub fn new(
system: impl Into<String>,
code_a: impl Into<String>,
code_b: impl Into<String>,
) -> Self {
Self {
system: system.into(),
code_a: code_a.into(),
code_b: code_b.into(),
version: None,
}
}
#[must_use]
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SubsumesOutcome {
Equivalent,
Subsumes,
SubsumedBy,
NotSubsumed,
}
impl SubsumesOutcome {
#[must_use]
pub fn from_code(code: &str) -> Option<Self> {
match code {
"equivalent" => Some(Self::Equivalent),
"subsumes" => Some(Self::Subsumes),
"subsumed-by" => Some(Self::SubsumedBy),
"not-subsumed" => Some(Self::NotSubsumed),
_ => None,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Equivalent => "equivalent",
Self::Subsumes => "subsumes",
Self::SubsumedBy => "subsumed-by",
Self::NotSubsumed => "not-subsumed",
}
}
}
#[cfg(test)]
mod tests {
use super::{
Expansion, Request, SubsumesOutcome, Subsumption, TranslateOutcome, ValidateOutcome,
};
use crate::concept::{Concept, Equivalence, Match};
fn found(equivalence: &str, code: &str) -> Match {
Match {
equivalence: Some(Equivalence::new(equivalence)),
concept: Some(Concept {
system: Some("http://example.org/target".to_owned()),
code: Some(code.to_owned()),
..Concept::default()
}),
source: None,
}
}
#[test]
fn accepted_yields_only_the_equivalent_and_equal_matches() {
let outcome = TranslateOutcome::Translated {
matches: vec![
found("wider", "broad"),
found("equivalent", "same"),
found("inexact", "close"),
found("equal", "identical"),
],
message: None,
};
let accepted: Vec<Option<String>> = outcome
.accepted()
.map(|found| {
found
.concept
.as_ref()
.and_then(|concept| concept.code.clone())
})
.collect();
assert_eq!(
vec![Some("same".to_owned()), Some("identical".to_owned())],
accepted
);
}
#[test]
fn a_no_match_outcome_accepts_nothing() {
let outcome = TranslateOutcome::NoMatch {
message: Some("no map".to_owned()),
};
assert_eq!(0, outcome.accepted().count());
}
#[test]
fn an_invalid_outcome_is_not_valid() {
let outcome = ValidateOutcome::Invalid {
message: Some("not in the value set".to_owned()),
outcome: None,
};
assert!(!outcome.is_valid());
}
#[test]
fn a_request_names_its_operation() {
let lookup = Request::Lookup {
system: "http://example.org/cs".to_owned(),
code: "a".to_owned(),
version: None,
date: None,
};
assert_eq!("CodeSystem/$lookup", lookup.operation());
assert_eq!(
"ValueSet/$expand",
Request::Expand(Expansion::new("http://example.org/vs")).operation()
);
assert_eq!(
"CodeSystem/$subsumes",
Request::Subsumes(Subsumption::new("http://example.org/cs", "a", "b")).operation()
);
}
#[test]
fn every_subsumption_outcome_reads_back_its_code() {
for code in ["equivalent", "subsumes", "subsumed-by", "not-subsumed"] {
let outcome = SubsumesOutcome::from_code(code).expect("a code of the value set");
assert_eq!(code, outcome.as_str());
}
assert_eq!(None, SubsumesOutcome::from_code("related"));
}
}