pub mod category;
pub mod conform;
pub mod displays;
mod entries;
#[cfg(feature = "openehr")]
pub mod openehr;
pub mod reference;
mod references;
pub(crate) mod strict;
pub mod validation;
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::bundle::Bundle;
use fhir_types::r4::composition::Composition;
use fhir_types::r4::patient::Patient;
use fhir_types::r4::reference::Reference;
use fhir_types::r4::resource::Resource;
use crate::receive::reference::ReferenceError;
use crate::receive::reference::Target;
pub const DOCUMENT: &str = "document";
#[derive(Debug, Clone)]
pub struct ReceivedDocument {
original: String,
tree: Object,
bundle: Bundle,
composition: Box<Composition>,
patient: Box<Patient>,
}
impl ReceivedDocument {
pub fn read(text: &str) -> Result<Self, ReceiveError> {
strict::unique_names(text).map_err(|source| {
if source.is_data() {
ReceiveError::RepeatedName { source }
} else {
ReceiveError::Json { source }
}
})?;
let value: Value =
serde_json::from_str(text).map_err(|source| ReceiveError::Json { source })?;
let Value::Object(read) = value else {
return Err(ReceiveError::NotAnObject);
};
if read.get("resourceType").and_then(Value::as_str) != Some("Bundle") {
return Err(ReceiveError::NotABundle);
}
let bundle = Bundle::from_json(&read, &mut Path::root("Bundle"))
.map_err(|source| ReceiveError::Decode { source })?;
let tree = Json::to_json(&bundle).map_err(|source| ReceiveError::Encode { source })?;
if tree != read {
return Err(ReceiveError::Unfaithful);
}
if bundle.r#type.value.as_deref() != Some(DOCUMENT) {
return Err(ReceiveError::NotADocument {
found: bundle.r#type.value.clone(),
});
}
let identified = bundle.identifier.as_ref().is_some_and(|identifier| {
identifier
.system
.as_ref()
.is_some_and(|system| system.value.is_some())
&& identifier
.value
.as_ref()
.is_some_and(|value| value.value.is_some())
});
if !identified {
return Err(ReceiveError::Unidentified);
}
if bundle
.timestamp
.as_ref()
.and_then(|timestamp| timestamp.value.as_ref())
.is_none()
{
return Err(ReceiveError::Undated);
}
entries::known_resources(&tree, "Bundle")?;
entries::full_urls(&tree)?;
let Some(Resource::Composition(composition)) = bundle
.entry
.first()
.and_then(|entry| entry.resource.as_ref())
else {
return Err(ReceiveError::NoComposition);
};
let composition = composition.clone();
let index = entries::patient(&tree)?;
let Some(Resource::Patient(patient)) = bundle
.entry
.get(index)
.and_then(|entry| entry.resource.as_ref())
else {
return Err(ReceiveError::SubjectUnresolved);
};
let patient = patient.clone();
Ok(Self {
original: text.to_owned(),
tree,
bundle,
composition,
patient,
})
}
#[must_use]
pub fn original(&self) -> &str {
&self.original
}
#[must_use]
pub const fn bundle(&self) -> &Bundle {
&self.bundle
}
#[must_use]
pub fn composition(&self) -> &Composition {
&self.composition
}
#[must_use]
pub fn patient(&self) -> &Patient {
&self.patient
}
pub fn resolve(&self, holder: usize, reference: &Reference) -> Result<Target, ReferenceError> {
let reference =
Json::to_json(reference).map_err(|source| ReferenceError::Unencodable { source })?;
reference::resolve_in(&reference::entries(&self.tree), holder, &reference)
}
pub(crate) const fn tree(&self) -> &Object {
&self.tree
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ReceiveError {
#[error("the document is not JSON")]
Json {
#[source]
source: serde_json::Error,
},
#[error("an object of the document repeats a name")]
RepeatedName {
#[source]
source: serde_json::Error,
},
#[error("the document is not a JSON object")]
NotAnObject,
#[error("the document is not a Bundle")]
NotABundle,
#[error("the Bundle does not decode against FHIR R4")]
Decode {
#[source]
source: DecodeError,
},
#[error("the decoded Bundle cannot be encoded as JSON")]
Encode {
#[source]
source: EncodeError,
},
#[error("the document reads differently through the R4 model")]
Unfaithful,
#[error("the Bundle is of type {found:?}, not a document")]
NotADocument {
found: Option<String>,
},
#[error("the document carries no identifier with a system and a value (bdl-9)")]
Unidentified,
#[error("the document carries no timestamp (bdl-10)")]
Undated,
#[error("the document's first entry is no Composition (bdl-11)")]
NoComposition,
#[error("the document's Composition names no subject")]
NoSubject,
#[error("the document's subject is no single Patient entry of the document")]
SubjectUnresolved,
#[error("{location} is a resource of a type FHIR R4 does not define")]
UnknownResource {
location: String,
},
#[error("entry {entry} has a version-specific fullUrl (bdl-8)")]
VersionedFullUrl {
entry: usize,
},
#[error("entry {entry} repeats the fullUrl of an earlier entry (bdl-7)")]
DuplicateFullUrl {
entry: usize,
},
#[error("entry {entry} has a fullUrl that disagrees with its resource's type or id")]
FullUrlMismatch {
entry: usize,
},
#[error("entry {entry} is a second Patient")]
SeveralPatients {
entry: usize,
},
#[error("{location} contains a Patient")]
ContainedPatient {
location: String,
},
#[error("{location} names a subject other than the document's Patient entry")]
SubjectMismatch {
location: String,
},
#[error("{location} is a reference the document cannot resolve")]
Reference {
location: String,
#[source]
source: ReferenceError,
},
#[error("entry {entry} has a fullUrl not spelled in its canonical form")]
NonCanonicalFullUrl {
entry: usize,
},
#[error("{location} is an element the R4 element table does not describe")]
Unreadable {
location: String,
},
}