use std::fmt;
use fhir_types::r4::operation_outcome::OperationOutcome;
use fhir_types::r4::schema::SCHEMAS;
use fhirconnect::engine::seam::Seams;
use fhirconnect::operations::contract::ToOpenehrRequest;
use fhirconnect::operations::error::OperationError;
use fhirconnect::operations::run;
use fhirconnect::operations::run::Settings;
use openehr_base::containers::NonEmptyVec;
use openehr_rm::v1_2::common::archetyped::feeder_audit::FeederAudit;
use openehr_rm::v1_2::common::archetyped::feeder_audit_details::FeederAuditDetails;
use openehr_rm::v1_2::common::generic::party_identified::{PartyIdentified, PartyIdentifiedData};
use openehr_rm::v1_2::common::generic::party_proxy::PartyProxy;
use openehr_rm::v1_2::common::generic::party_self::PartySelf;
use openehr_rm::v1_2::composition::composition::Composition;
use openehr_rm::v1_2::composition::content::content_item::ContentItem;
use openehr_rm::v1_2::data_types::basic::dv_identifier::DvIdentifier;
use openehr_rm::v1_2::data_types::encapsulated::dv_encapsulated::DvEncapsulated;
use openehr_rm::v1_2::data_types::encapsulated::dv_parsable::DvParsable;
use crate::mapping::Mapping;
use crate::receive::ReceivedDocument;
pub const FHIR_JSON: &str = "application/fhir+json";
#[derive(Debug, Clone)]
pub struct ReceivedComposition {
model: Composition,
composition: String,
template_id: String,
outcome: Option<OperationOutcome>,
}
impl PartialEq for ReceivedComposition {
fn eq(&self, other: &Self) -> bool {
self.composition == other.composition
&& self.template_id == other.template_id
&& self.outcome == other.outcome
}
}
impl Eq for ReceivedComposition {}
impl ReceivedComposition {
#[must_use]
pub fn composition(&self) -> &str {
&self.composition
}
#[must_use]
pub const fn model(&self) -> &Composition {
&self.model
}
#[must_use]
pub fn template_id(&self) -> &str {
&self.template_id
}
#[must_use]
pub const fn outcome(&self) -> Option<&OperationOutcome> {
self.outcome.as_ref()
}
}
impl Mapping {
pub fn to_openehr(
&self,
document: &ReceivedDocument,
settings: &Settings,
) -> Result<ReceivedComposition, ReceiveMappingError> {
let request = ToOpenehrRequest::new(document.bundle().clone());
let response = run::to_openehr(
self.programs(),
&SCHEMAS,
&Seams::default(),
settings,
&request,
)
.map_err(|source| ReceiveMappingError::Run {
source: Box::new(source),
})?;
let mut composition: Composition = serde_json::from_str(response.composition())
.map_err(|source| ReceiveMappingError::Composition { source })?;
let template_id = composition
.archetype_details
.as_ref()
.and_then(|details| details.template_id.as_ref())
.map(|template| template.value.clone())
.ok_or(ReceiveMappingError::Untemplated)?;
let audit = composition.feeder_audit.get_or_insert_with(|| FeederAudit {
originating_system_item_ids: None,
feeder_system_item_ids: None,
original_content: None,
originating_system_audit: Box::new(FeederAuditDetails {
system_id: settings.device().to_owned(),
location: None,
subject: None,
provider: None,
time: None,
version_id: None,
other_details: None,
}),
feeder_system_audit: None,
});
if audit.original_content.is_some() {
return Err(ReceiveMappingError::OriginalTaken);
}
audit.original_content = Some(DvEncapsulated::DvParsable(DvParsable {
charset: None,
language: None,
value: document.original().to_owned(),
formalism: FHIR_JSON.to_owned(),
}));
let canonical = serde_json::to_string(&composition)
.map_err(|source| ReceiveMappingError::Encode { source })?;
Ok(ReceivedComposition {
model: composition,
composition: canonical,
template_id,
outcome: response.outcome().cloned(),
})
}
}
impl ReceivedComposition {
pub fn inserted_by_subject(self) -> Result<Self, SubjectInsertionError> {
self.provided_by(&PartyProxy::PartySelf(PartySelf { external_ref: None }))
}
pub fn inserted_by_representative(
self,
representative: &Representative,
) -> Result<Self, SubjectInsertionError> {
self.provided_by(&PartyProxy::PartyIdentified(
PartyIdentified::PartyIdentified(representative.party.clone()),
))
}
fn provided_by(mut self, provider: &PartyProxy) -> Result<Self, SubjectInsertionError> {
let marked = match self.model.content.as_mut() {
Some(content) => mark(content, provider)?,
None => 0,
};
if marked == 0 {
return Err(SubjectInsertionError::NoEntry);
}
self.composition = serde_json::to_string(&self.model)
.map_err(|source| SubjectInsertionError::Encode { source })?;
Ok(self)
}
}
#[derive(Clone, PartialEq)]
pub struct Representative {
party: PartyIdentifiedData,
}
impl Representative {
#[must_use]
pub fn new(name: Option<String>, identifier: Option<DvIdentifier>) -> Option<Self> {
let name = name.filter(|name| !name.is_empty());
let identifiers = identifier
.filter(|identifier| !identifier.id.is_empty())
.map(NonEmptyVec::of);
if name.is_none() && identifiers.is_none() {
return None;
}
Some(Self {
party: PartyIdentifiedData {
external_ref: None,
name,
identifiers,
},
})
}
}
impl fmt::Debug for Representative {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Representative")
.field("name", &self.party.name.is_some())
.field("identifier", &self.party.identifiers.is_some())
.finish()
}
}
fn mark(items: &mut [ContentItem], provider: &PartyProxy) -> Result<usize, SubjectInsertionError> {
let mut marked = 0_usize;
for item in items {
let named = match item {
ContentItem::Section(section) => {
let within = match section.items.as_mut() {
Some(items) => mark(items, provider)?,
None => 0,
};
marked = marked.saturating_add(within);
continue;
}
ContentItem::GenericEntry(_) => return Err(SubjectInsertionError::GenericEntry),
ContentItem::Action(entry) => &mut entry.provider,
ContentItem::AdminEntry(entry) => &mut entry.provider,
ContentItem::Evaluation(entry) => &mut entry.provider,
ContentItem::Instruction(entry) => &mut entry.provider,
ContentItem::Observation(entry) => &mut entry.provider,
};
*named = Some(provider.clone());
marked = marked.saturating_add(1);
}
Ok(marked)
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SubjectInsertionError {
#[error("the composition holds no entry a provider can be named in")]
NoEntry,
#[error("the composition holds a GENERIC_ENTRY, which names no provider")]
GenericEntry,
#[error("the marked composition cannot be written as canonical JSON")]
Encode {
#[source]
source: serde_json::Error,
},
}
#[cfg(test)]
#[expect(
clippy::panic_in_result_fn,
reason = "a test asserts, and returns its setup errors"
)]
mod tests {
use std::error::Error;
use openehr_rm::v1_2::composition::composition::Composition;
use super::{ReceivedComposition, SubjectInsertionError};
fn composition(content: &str) -> Result<ReceivedComposition, Box<dyn Error>> {
let text = format!(
r#"{{
"_type": "COMPOSITION",
"name": {{ "_type": "DV_TEXT", "value": "Synthetic" }},
"archetype_node_id": "openEHR-EHR-COMPOSITION.synthetic.v1",
"archetype_details": {{
"_type": "ARCHETYPED",
"archetype_id": {{ "_type": "ARCHETYPE_ID", "value": "openEHR-EHR-COMPOSITION.synthetic.v1" }},
"template_id": {{ "_type": "TEMPLATE_ID", "value": "synthetic.v1" }},
"rm_version": "1.1.0"
}},
"language": {{ "_type": "CODE_PHRASE", "terminology_id": {{ "_type": "TERMINOLOGY_ID", "value": "ISO_639-1" }}, "code_string": "en" }},
"territory": {{ "_type": "CODE_PHRASE", "terminology_id": {{ "_type": "TERMINOLOGY_ID", "value": "ISO_3166-1" }}, "code_string": "NL" }},
"category": {{ "_type": "DV_CODED_TEXT", "value": "persistent", "defining_code": {{ "_type": "CODE_PHRASE", "terminology_id": {{ "_type": "TERMINOLOGY_ID", "value": "openehr" }}, "code_string": "431" }} }},
"composer": {{ "_type": "PARTY_SELF" }}{content}
}}"#
);
let model: Composition = serde_json::from_str(&text)?;
Ok(ReceivedComposition {
composition: serde_json::to_string(&model)?,
model,
template_id: String::from("synthetic.v1"),
outcome: None,
})
}
#[test]
fn a_composition_with_no_entry_cannot_carry_the_mark() -> Result<(), Box<dyn Error>> {
let refused = composition("")?.inserted_by_subject();
assert!(
matches!(refused, Err(SubjectInsertionError::NoEntry)),
"{refused:?}"
);
let empty = composition(
r#",
"content": [{ "_type": "SECTION", "name": { "_type": "DV_TEXT", "value": "Empty" }, "archetype_node_id": "openEHR-EHR-SECTION.synthetic.v1" }]"#,
)?;
let refused = empty.inserted_by_subject();
assert!(
matches!(refused, Err(SubjectInsertionError::NoEntry)),
"a section with no entry carries no mark: {refused:?}"
);
Ok(())
}
#[test]
fn a_generic_entry_cannot_carry_the_mark() -> Result<(), Box<dyn Error>> {
let generic = composition(
r#",
"content": [{
"_type": "GENERIC_ENTRY",
"name": { "_type": "DV_TEXT", "value": "Legacy" },
"archetype_node_id": "openEHR-EHR-GENERIC_ENTRY.synthetic.v1",
"data": { "_type": "ELEMENT", "name": { "_type": "DV_TEXT", "value": "Item" }, "archetype_node_id": "at0001" }
}]"#,
)?;
let refused = generic.inserted_by_subject();
assert!(
matches!(refused, Err(SubjectInsertionError::GenericEntry)),
"{refused:?}"
);
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ReceiveMappingError {
#[error("the mapping run was refused")]
Run {
#[source]
source: Box<OperationError>,
},
#[error("the mapped composition does not read as RM canonical JSON")]
Composition {
#[source]
source: serde_json::Error,
},
#[error("the mapped composition names no template")]
Untemplated,
#[error("the mapping already wrote the composition's original content")]
OriginalTaken,
#[error("the composition cannot be written as canonical JSON")]
Encode {
#[source]
source: serde_json::Error,
},
}