#[cfg(feature = "dispensation")]
pub mod dispensation;
#[cfg(feature = "prescription")]
pub mod prescription;
use std::io::Cursor;
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::annotation::Annotation;
use fhir_types::r4::codeable_concept::CodeableConcept;
use fhir_types::r4::extension::Extension;
use fhir_types::r4::extension::ExtensionValue;
use fhir_types::r4::medication::Medication;
use fhir_types::r4::meta::Meta;
use fhir_types::r4::reference::Reference;
use fhir_types::r4::resource::Resource;
use crate::dataset::DatasetError;
use crate::dataset::ResourceProfile;
use crate::receive::conform::CheckError;
use crate::receive::conform::Conformance;
use crate::receive::conform::Unevaluated;
use crate::receive::conform::check_resource;
use crate::receive::strict;
use crate::terminology::bindings::BindingError;
use crate::terminology::bindings::check_required;
use crate::terminology::expansion::Expansions;
pub const MPD_MEDICATION: &str = "http://hl7.eu/fhir/mpd/StructureDefinition/Medication-eu-mpd";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Medicinal {
Coded(CodeableConcept),
Referenced(Reference),
Contained {
id: String,
medication: Box<Medication>,
},
}
impl Medicinal {
fn read(
coded: Option<&CodeableConcept>,
reference: Option<&Reference>,
contained: &[Resource],
element: &'static str,
) -> Result<Self, ReadError> {
if let Some(coded) = coded {
return Ok(Self::Coded(coded.clone()));
}
let reference = reference.ok_or(ReadError::Missing { element })?;
let Some(local) = reference
.reference
.as_ref()
.and_then(|text| text.value.as_deref())
.and_then(|text| text.strip_prefix('#'))
else {
return Ok(Self::Referenced(reference.clone()));
};
let mut found = contained.iter().filter_map(|resource| match resource {
Resource::Medication(medication) if medication.id.as_deref() == Some(local) => {
Some(medication)
}
_ => None,
});
match (found.next(), found.next()) {
(Some(medication), None) => {
let mut medication = medication.clone();
medication.id = None;
Ok(Self::Contained {
id: local.to_owned(),
medication,
})
}
_ => Err(ReadError::Dangling { element }),
}
}
fn write(&self) -> (Written, Option<Resource>) {
match self {
Self::Coded(coded) => (Written::Coded(Box::new(coded.clone())), None),
Self::Referenced(reference) => (Written::Referenced(Box::new(reference.clone())), None),
Self::Contained { id, medication } => {
let mut medication = medication.clone();
medication.id = Some(id.clone());
let reference = Reference {
reference: Some(format!("#{id}").into()),
..Reference::default()
};
(
Written::Referenced(Box::new(reference)),
Some(Resource::Medication(medication)),
)
}
}
}
}
enum Written {
Coded(Box<CodeableConcept>),
Referenced(Box<Reference>),
}
#[derive(Debug, Clone)]
pub struct Mpd {
#[cfg(feature = "prescription")]
request: ResourceProfile,
#[cfg(feature = "dispensation")]
dispense: ResourceProfile,
medication: ResourceProfile,
}
impl Mpd {
pub fn read(archive: &[u8]) -> Result<Self, DatasetError> {
let profile = |url| ResourceProfile::read(Cursor::new(archive), url);
Ok(Self {
#[cfg(feature = "prescription")]
request: profile(crate::crosswalk::prescription::MPD_MEDICATION_REQUEST)?,
#[cfg(feature = "dispensation")]
dispense: profile(crate::crosswalk::dispensation::MPD_MEDICATION_DISPENSE)?,
medication: profile(MPD_MEDICATION)?,
})
}
fn check(
&self,
profile: &ResourceProfile,
expected: &'static str,
resource: &Object,
) -> Result<Conformance, CheckError> {
let mut findings = Vec::new();
let mut passed = Conformance::default();
let contained = resource
.get("contained")
.and_then(Value::as_array)
.unwrap_or_default()
.iter()
.filter_map(Value::as_object)
.filter(|object| {
object.get("resourceType").and_then(Value::as_str) == Some("Medication")
});
let checks = std::iter::once((profile, expected, resource))
.chain(contained.map(|medication| (&self.medication, "Medication", medication)));
for (profile, expected, resource) in checks {
let check = check_resource(profile, expected, resource).and_then(|conformance| {
Ok(conformance.join(bindings(profile, expected, resource)?))
});
match check {
Ok(conformance) => passed = passed.join(conformance),
Err(CheckError::NonConformant { findings: found }) => findings.extend(found),
Err(other) => return Err(other),
}
}
if findings.is_empty() {
Ok(passed)
} else {
Err(CheckError::NonConformant { findings })
}
}
}
fn bindings(
profile: &ResourceProfile,
expected: &'static str,
resource: &Object,
) -> Result<Conformance, CheckError> {
let check =
check_required(profile, resource, expected, &Expansions::default()).map_err(|error| {
match error {
BindingError::Unreadable { location } | BindingError::Decode { location, .. } => {
CheckError::Unreadable { location }
}
}
})?;
Ok(Conformance::listing(
check.unevaluated().iter().cloned().map(Unevaluated::from),
))
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ReadError {
#[error("the resource is not JSON")]
Json {
#[source]
source: serde_json::Error,
},
#[error("an object of the resource repeats a name")]
RepeatedName {
#[source]
source: serde_json::Error,
},
#[error("the resource is not a JSON object")]
NotAnObject,
#[error("the resource is not a {expected}")]
WrongType {
expected: &'static str,
},
#[error("the resource does not decode against FHIR R4")]
Decode {
#[source]
source: DecodeError,
},
#[error("the decoded resource cannot be encoded as JSON")]
Encode {
#[source]
source: EncodeError,
},
#[error("the resource reads differently through the R4 model")]
Unfaithful,
#[error("{element} is absent, and the dataset requires it")]
Missing {
element: &'static str,
},
#[error("{element} repeats, and the dataset admits one")]
Repeated {
element: &'static str,
},
#[error("the extension {url} carries no single value of its type")]
Extension {
url: &'static str,
},
#[error("{element} names no single contained resource")]
Dangling {
element: &'static str,
},
#[error("the resources disagree about {element}")]
Disagree {
element: &'static str,
},
}
fn decode<T: Json>(text: &str, expected: &'static str) -> Result<T, ReadError> {
strict::unique_names(text).map_err(|source| {
if source.is_data() {
ReadError::RepeatedName { source }
} else {
ReadError::Json { source }
}
})?;
let value: Value = serde_json::from_str(text).map_err(|source| ReadError::Json { source })?;
let Value::Object(read) = value else {
return Err(ReadError::NotAnObject);
};
if read.get("resourceType").and_then(Value::as_str) != Some(expected) {
return Err(ReadError::WrongType { expected });
}
let resource = T::from_json(&read, &mut Path::root(expected))
.map_err(|source| ReadError::Decode { source })?;
let again = resource
.to_json()
.map_err(|source| ReadError::Encode { source })?;
if again != read {
return Err(ReadError::Unfaithful);
}
Ok(resource)
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WriteError {
#[error("{element} is empty, and the dataset requires it")]
Empty {
element: &'static str,
},
#[error("{element} holds more than the MPD profile carries")]
Unrepresentable {
element: &'static str,
},
#[error("the resource cannot be encoded as JSON")]
Encode {
#[source]
source: EncodeError,
},
}
fn extension<T>(
extensions: &[Extension],
url: &'static str,
value: impl Fn(&ExtensionValue) -> Option<T>,
) -> Result<Option<T>, ReadError> {
let mut found = extensions.iter().filter(|extension| extension.url == url);
match (found.next(), found.next()) {
(None, _) => Ok(None),
(Some(only), None) => only
.value
.as_ref()
.and_then(value)
.map(Some)
.ok_or(ReadError::Extension { url }),
(Some(_), Some(_)) => Err(ReadError::Extension { url }),
}
}
fn single<'a, T>(list: &'a [T], element: &'static str) -> Result<Option<&'a T>, ReadError> {
match list {
[] => Ok(None),
[only] => Ok(Some(only)),
_ => Err(ReadError::Repeated { element }),
}
}
fn note(notes: &[Annotation], element: &'static str) -> Result<Option<String>, ReadError> {
Ok(single(notes, element)?.and_then(|note| note.text.value.clone()))
}
fn annotation(text: Option<&String>) -> Vec<Annotation> {
text.map(|text| Annotation {
text: text.as_str().into(),
..Annotation::default()
})
.into_iter()
.collect()
}
fn claiming(profile: &str) -> Meta {
Meta {
profile: vec![profile.into()],
..Meta::default()
}
}