pub mod bindings;
pub mod ehn;
pub mod expansion;
#[cfg(feature = "openehr")]
pub mod local;
#[cfg(feature = "openehr")]
pub mod system;
#[cfg(feature = "openehr")]
pub mod transcode;
pub mod translation;
pub(crate) mod walk;
#[cfg(feature = "openehr")]
pub mod withheld;
use std::fmt;
use std::sync::Arc;
use async_trait::async_trait;
use fhir_types::r4::codeable_concept::CodeableConcept;
use fhir_types::r4::coding::Coding;
use fhir_types::r4::operations::concept_map_translate::ConceptMapTranslateResponse;
use http::StatusCode;
pub const ACCEPTED_EQUIVALENCES: [&str; 2] = ["equivalent", "equal"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Match {
pub equivalence: Option<String>,
pub concept: Option<Coding>,
}
#[async_trait]
pub trait TerminologyServer: fmt::Debug + Send + Sync {
async fn translate(
&self,
system: &str,
code: &str,
target: &str,
) -> Result<Vec<Match>, TerminologyError>;
async fn lookup(&self, system: &str, code: &str) -> Result<String, TerminologyError>;
async fn validate_code(
&self,
value_set: &str,
concept: &Candidate,
) -> Result<bool, TerminologyError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Operation {
Translate,
Lookup,
ValidateCode,
}
impl Operation {
#[must_use]
pub const fn path(self) -> &'static str {
match self {
Self::Translate => "ConceptMap/$translate",
Self::Lookup => "CodeSystem/$lookup",
Self::ValidateCode => "ValueSet/$validate-code",
}
}
}
impl fmt::Display for Operation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.path())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TerminologyError {
#[error("no terminology server is configured, so {operation} cannot be asked")]
Unconfigured {
operation: Operation,
},
#[error("the terminology server answered {operation} with {status}")]
Status {
operation: Operation,
status: StatusCode,
},
#[error("the terminology server did not answer {operation} in time")]
Timeout {
operation: Operation,
},
#[error("the terminology server could not be reached for {operation}")]
Transport {
operation: Operation,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("the terminology server does not know the code asked of {operation}")]
NotFound {
operation: Operation,
},
#[error("the terminology server's answer to {operation} does not read as its Parameters")]
Answer {
operation: Operation,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
}
#[derive(Debug, Clone, Default)]
pub struct Terminology {
server: Option<Arc<dyn TerminologyServer>>,
}
impl Terminology {
#[must_use]
pub const fn none() -> Self {
Self { server: None }
}
#[must_use]
pub fn server(server: Arc<dyn TerminologyServer>) -> Self {
Self {
server: Some(server),
}
}
#[must_use]
pub const fn is_configured(&self) -> bool {
self.server.is_some()
}
fn ask(&self, operation: Operation) -> Result<&dyn TerminologyServer, TerminologyError> {
self.server
.as_deref()
.ok_or(TerminologyError::Unconfigured { operation })
}
pub async fn translate(
&self,
system: &str,
code: &str,
target: &str,
) -> Result<Vec<Coding>, TerminologyError> {
let server = self.ask(Operation::Translate)?;
let matches = server.translate(system, code, target).await?;
Ok(matches
.into_iter()
.filter(|found| {
found
.equivalence
.as_deref()
.is_some_and(|code| ACCEPTED_EQUIVALENCES.contains(&code))
})
.filter_map(|found| found.concept)
.collect())
}
pub async fn display(&self, system: &str, code: &str) -> Result<String, TerminologyError> {
let server = self.ask(Operation::Lookup)?;
let display = server.lookup(system, code).await?;
Some(display)
.filter(|display| !display.is_empty())
.ok_or_else(|| TerminologyError::Answer {
operation: Operation::Lookup,
source: Box::new(EmptyDisplay),
})
}
pub async fn validate(
&self,
value_set: &str,
concept: &Candidate,
) -> Result<bool, TerminologyError> {
let server = self.ask(Operation::ValidateCode)?;
server.validate_code(value_set, concept).await
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Candidate {
Code {
code: String,
context: String,
},
Coding(Box<Coding>),
Concept(Box<CodeableConcept>),
}
#[must_use]
pub fn accepted(response: &ConceptMapTranslateResponse) -> Vec<Coding> {
if response.result.value != Some(true) {
return Vec::new();
}
response
.r#match
.iter()
.filter(|found| {
found
.equivalence
.as_ref()
.and_then(|code| code.value.as_deref())
.is_some_and(|code| ACCEPTED_EQUIVALENCES.contains(&code))
})
.filter_map(|found| found.concept.clone())
.collect()
}
#[derive(Debug, thiserror::Error)]
#[error("the answer carries no display")]
struct EmptyDisplay;