use std::collections::BTreeMap;
use fhir_types::codec::DecodeError;
use fhir_types::codec::EncodeError;
use fhir_types::codec::Json;
use fhir_types::codec::Object;
use fhir_types::codec::Path;
use fhir_types::codec::Value;
use fhir_types::r4::coding::Coding;
use fhir_types::r4::resource::Resource;
use crate::terminology::Terminology;
use crate::terminology::TerminologyError;
use crate::terminology::walk;
use crate::terminology::walk::Coded;
use crate::terminology::walk::Step;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TranslationError {
#[error("the resource does not encode as FHIR JSON")]
Encode {
#[source]
source: EncodeError,
},
#[error("{location} is an element the R4 element table does not describe")]
Unwalkable {
location: String,
},
#[error("the resource with its translations does not decode")]
Decode {
#[source]
source: DecodeError,
},
#[error("the terminology server gave no translation")]
Terminology(#[from] TerminologyError),
}
struct Pending {
location: String,
steps: Vec<Step>,
originals: Vec<(String, String)>,
}
#[derive(Debug)]
pub struct Translator<'t> {
terminology: &'t Terminology,
targets: &'t [&'t str],
asked: BTreeMap<(String, String, String), Vec<Coding>>,
}
impl<'t> Translator<'t> {
#[must_use]
pub const fn new(terminology: &'t Terminology, targets: &'t [&'t str]) -> Self {
Self {
terminology,
targets,
asked: BTreeMap::new(),
}
}
pub async fn translate(
&mut self,
resource: &Resource,
preferred: impl Fn(&str) -> bool + Sync,
) -> Result<Resource, TranslationError> {
if !self.terminology.is_configured() {
return Ok(resource.clone());
}
let mut object =
Json::to_json(resource).map_err(|source| TranslationError::Encode { source })?;
let kind = object
.get("resourceType")
.and_then(Value::as_str)
.unwrap_or("Resource")
.to_owned();
let pending = untranscoded(&object, &kind, &preferred)?;
if pending.is_empty() {
return Ok(resource.clone());
}
let mut changed = false;
for concept in pending {
let added = self.equivalents(&concept.originals).await?;
if added.is_empty() {
continue;
}
changed = true;
let codings = codings_at(&mut object, &concept.steps).ok_or_else(|| {
TranslationError::Unwalkable {
location: concept.location.clone(),
}
})?;
for coding in &added {
let encoded =
Json::to_json(coding).map_err(|source| TranslationError::Encode { source })?;
codings.push(Value::Object(encoded));
}
}
if !changed {
return Ok(resource.clone());
}
Resource::from_json(&object, &mut Path::root(&kind))
.map_err(|source| TranslationError::Decode { source })
}
async fn equivalents(
&mut self,
originals: &[(String, String)],
) -> Result<Vec<Coding>, TerminologyError> {
let mut added: Vec<Coding> = Vec::new();
for (system, code) in originals {
for target in self.targets {
let key = (system.clone(), code.clone(), (*target).to_owned());
if !self.asked.contains_key(&key) {
let found = self.terminology.translate(system, code, target).await?;
self.asked.insert(key.clone(), found);
}
for found in self.asked.get(&key).into_iter().flatten() {
let pair = identity(found);
let known = pair.is_some_and(|(system, code)| {
originals
.iter()
.any(|(held, value)| held == system && value == code)
});
if !known && !added.iter().any(|held| identity(held) == pair) {
added.push(found.clone());
}
}
}
}
Ok(added)
}
}
fn untranscoded(
object: &Object,
kind: &str,
preferred: &impl Fn(&str) -> bool,
) -> Result<Vec<Pending>, TranslationError> {
let found =
walk::coded(object, kind, false).map_err(|unwalkable| TranslationError::Unwalkable {
location: unwalkable.location,
})?;
let mut pending = Vec::new();
for occurrence in found {
if occurrence.element.starts_with("Meta.") {
continue;
}
let Coded::Concept(concept) = occurrence.value else {
continue;
};
let codings: Vec<&Object> = concept
.get("coding")
.and_then(Value::as_array)
.map(|codings| codings.iter().filter_map(Value::as_object).collect())
.unwrap_or_default();
let carried = codings.iter().any(|coding| {
coding
.get("system")
.and_then(Value::as_str)
.is_some_and(preferred)
});
if carried {
continue;
}
let originals: Vec<(String, String)> = codings
.iter()
.filter_map(|coding| {
let system = coding.get("system").and_then(Value::as_str)?;
let code = coding.get("code").and_then(Value::as_str)?;
Some((system.to_owned(), code.to_owned()))
})
.collect();
if originals.is_empty() {
continue;
}
pending.push(Pending {
location: occurrence.location,
steps: occurrence.steps,
originals,
});
}
Ok(pending)
}
fn codings_at<'o>(object: &'o mut Object, steps: &[Step]) -> Option<&'o mut Vec<Value>> {
let (first, rest) = steps.split_first()?;
let Step::Key(key) = first else {
return None;
};
let mut node = object.get_mut(key)?;
for step in rest {
node = match (step, node) {
(Step::Key(key), Value::Object(inner)) => inner.get_mut(key)?,
(Step::Index(index), Value::Array(items)) => items.get_mut(*index)?,
_ => return None,
};
}
let Value::Object(concept) = node else {
return None;
};
match concept.get_mut("coding")? {
Value::Array(codings) => Some(codings),
_ => None,
}
}
fn identity(coding: &Coding) -> Option<(&str, &str)> {
let system = coding.system.as_ref()?.value.as_deref()?;
let code = coding.code.as_ref()?.value.as_deref()?;
Some((system, code))
}