use crate::base::{HierObjectId, LocatableRef, ObjectRef};
use crate::error::ParseError;
use crate::rm::common::{
Locatable, LocatableAttrs, Participation, PartyProxy, PartySelf, impl_locatable,
};
use crate::rm::data_structures::{History, ItemStructure};
use crate::rm::data_types::{
CodePhrase, DvCodedText, DvDateTime, DvOrdered as _, DvParsable, DvText, Text,
};
use crate::rm::rm_type_tag;
use crate::terminology;
use serde::{Deserialize, Serialize};
rm_type_tag!(CompositionTag, "COMPOSITION");
rm_type_tag!(EhrStatusTag, "EHR_STATUS");
rm_type_tag!(FolderTag, "FOLDER");
rm_type_tag!(EventContextTag, "EVENT_CONTEXT");
rm_type_tag!(ActivityTag, "ACTIVITY");
rm_type_tag!(IsmTransitionTag, "ISM_TRANSITION");
rm_type_tag!(InstructionDetailsTag, "INSTRUCTION_DETAILS");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(clippy::struct_field_names)]
pub struct Ehr {
system_id: HierObjectId,
ehr_id: HierObjectId,
ehr_status: ObjectRef,
ehr_access: ObjectRef,
time_created: DvDateTime,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
compositions: Vec<ObjectRef>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
contributions: Vec<ObjectRef>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
folders: Vec<ObjectRef>,
#[serde(skip_serializing_if = "Option::is_none", default)]
directory: Option<ObjectRef>,
}
impl Ehr {
pub fn new(
system_id: HierObjectId,
ehr_id: HierObjectId,
ehr_status: ObjectRef,
ehr_access: ObjectRef,
time_created: DvDateTime,
) -> Result<Self, ParseError> {
if ehr_status.type_name() != "VERSIONED_EHR_STATUS" {
return Err(ParseError::invariant("EHR", "Ehr_status_valid"));
}
if ehr_access.type_name() != "VERSIONED_EHR_ACCESS" {
return Err(ParseError::invariant("EHR", "Ehr_access_valid"));
}
Ok(Self {
system_id,
ehr_id,
ehr_status,
ehr_access,
time_created,
compositions: Vec::new(),
contributions: Vec::new(),
folders: Vec::new(),
directory: None,
})
}
#[must_use]
pub fn with_composition(mut self, composition: ObjectRef) -> Self {
self.compositions.push(composition);
self
}
#[must_use]
pub fn with_contribution(mut self, contribution: ObjectRef) -> Self {
self.contributions.push(contribution);
self
}
pub fn with_folders(mut self, folders: Vec<ObjectRef>) -> Result<Self, ParseError> {
let Some(first) = folders.first().cloned() else {
return Err(ParseError::invariant("EHR", "Directory_in_folders"));
};
self.directory = Some(first);
self.folders = folders;
Ok(self)
}
#[must_use]
pub fn ehr_id(&self) -> &HierObjectId {
&self.ehr_id
}
#[must_use]
pub fn system_id(&self) -> &HierObjectId {
&self.system_id
}
#[must_use]
pub fn ehr_status(&self) -> &ObjectRef {
&self.ehr_status
}
#[must_use]
pub fn ehr_access(&self) -> &ObjectRef {
&self.ehr_access
}
#[must_use]
pub fn time_created(&self) -> &DvDateTime {
&self.time_created
}
#[must_use]
pub fn compositions(&self) -> &[ObjectRef] {
&self.compositions
}
#[must_use]
pub fn contributions(&self) -> &[ObjectRef] {
&self.contributions
}
#[must_use]
pub fn folders(&self) -> &[ObjectRef] {
&self.folders
}
#[must_use]
pub fn directory(&self) -> Option<&ObjectRef> {
self.directory.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EhrStatus {
#[serde(rename = "_type", default)]
rm_type: EhrStatusTag,
#[serde(flatten)]
locatable: LocatableAttrs,
subject: PartySelf,
is_queryable: bool,
is_modifiable: bool,
#[serde(skip_serializing_if = "Option::is_none", default)]
other_details: Option<Box<ItemStructure>>,
}
impl_locatable!(EhrStatus, "EHR_STATUS");
impl EhrStatus {
#[must_use]
pub fn new(
locatable: LocatableAttrs,
subject: PartySelf,
is_queryable: bool,
is_modifiable: bool,
) -> Self {
Self {
locatable,
subject,
rm_type: EhrStatusTag,
is_queryable,
is_modifiable,
other_details: None,
}
}
#[must_use]
pub fn with_other_details(mut self, other_details: ItemStructure) -> Self {
self.other_details = Some(Box::new(other_details));
self
}
#[must_use]
pub fn subject(&self) -> &PartySelf {
&self.subject
}
#[must_use]
pub fn is_queryable(&self) -> bool {
self.is_queryable
}
#[must_use]
pub fn is_modifiable(&self) -> bool {
self.is_modifiable
}
#[must_use]
pub fn other_details(&self) -> Option<&ItemStructure> {
self.other_details.as_deref()
}
#[must_use]
pub fn is_active(&self) -> bool {
self.is_modifiable
}
#[must_use]
pub fn set_modifiable(mut self, modifiable: bool) -> Self {
self.is_modifiable = modifiable;
self
}
#[must_use]
pub fn set_queryable(mut self, queryable: bool) -> Self {
self.is_queryable = queryable;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Folder {
#[serde(rename = "_type", default)]
rm_type: FolderTag,
#[serde(flatten)]
locatable: LocatableAttrs,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
items: Vec<ObjectRef>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
folders: Vec<Folder>,
#[serde(skip_serializing_if = "Option::is_none", default)]
details: Option<ItemStructure>,
}
impl_locatable!(Folder, "FOLDER");
impl Folder {
#[must_use]
pub fn new(locatable: LocatableAttrs) -> Self {
Self {
locatable,
rm_type: FolderTag,
items: Vec::new(),
folders: Vec::new(),
details: None,
}
}
#[must_use]
pub fn with_item(mut self, item: ObjectRef) -> Self {
self.items.push(item);
self
}
#[must_use]
pub fn with_folder(mut self, folder: Folder) -> Self {
self.folders.push(folder);
self
}
#[must_use]
pub fn items(&self) -> &[ObjectRef] {
&self.items
}
#[must_use]
pub fn folders(&self) -> &[Folder] {
&self.folders
}
#[must_use]
pub fn details(&self) -> Option<&ItemStructure> {
self.details.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EventContext {
#[serde(rename = "_type", default)]
rm_type: EventContextTag,
start_time: DvDateTime,
#[serde(skip_serializing_if = "Option::is_none", default)]
end_time: Option<DvDateTime>,
#[serde(skip_serializing_if = "Option::is_none", default)]
location: Option<String>,
setting: DvCodedText,
#[serde(skip_serializing_if = "Option::is_none", default)]
health_care_facility: Option<crate::rm::common::PartyIdentified>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
participations: Vec<Participation>,
#[serde(skip_serializing_if = "Option::is_none", default)]
other_context: Option<Box<ItemStructure>>,
}
impl EventContext {
pub fn new(start_time: DvDateTime, setting_code: &str) -> Result<Self, ParseError> {
let setting = terminology::setting::GROUP
.coded_text(setting_code)
.ok_or_else(|| ParseError::invariant("EVENT_CONTEXT", "Setting_valid"))?;
Ok(Self {
start_time,
rm_type: EventContextTag,
end_time: None,
location: None,
setting,
health_care_facility: None,
participations: Vec::new(),
other_context: None,
})
}
pub fn with_end_time(mut self, end_time: DvDateTime) -> Result<Self, ParseError> {
if matches!(
end_time.semantic_cmp(&self.start_time),
Some(core::cmp::Ordering::Less)
) {
return Err(ParseError::invariant("EVENT_CONTEXT", "End_time_valid"));
}
self.end_time = Some(end_time);
Ok(self)
}
pub fn with_location(mut self, location: impl Into<String>) -> Result<Self, ParseError> {
let location = location.into();
if location.is_empty() {
return Err(ParseError::invariant("EVENT_CONTEXT", "location_valid"));
}
self.location = Some(location);
Ok(self)
}
#[must_use]
pub fn with_participation(mut self, participation: Participation) -> Self {
self.participations.push(participation);
self
}
#[must_use]
pub fn with_other_context(mut self, other_context: ItemStructure) -> Self {
self.other_context = Some(Box::new(other_context));
self
}
#[must_use]
pub fn start_time(&self) -> &DvDateTime {
&self.start_time
}
#[must_use]
pub fn end_time(&self) -> Option<&DvDateTime> {
self.end_time.as_ref()
}
#[must_use]
pub fn setting(&self) -> &DvCodedText {
&self.setting
}
#[must_use]
pub fn location(&self) -> Option<&str> {
self.location.as_deref()
}
#[must_use]
pub fn participations(&self) -> &[Participation] {
&self.participations
}
#[must_use]
pub fn other_context(&self) -> Option<&ItemStructure> {
self.other_context.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Composition {
#[serde(rename = "_type", default)]
rm_type: CompositionTag,
#[serde(flatten)]
locatable: LocatableAttrs,
language: CodePhrase,
territory: CodePhrase,
category: DvCodedText,
composer: PartyProxy,
#[serde(skip_serializing_if = "Option::is_none", default)]
context: Option<Box<EventContext>>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
content: Vec<ContentItem>,
}
impl_locatable!(Composition, "COMPOSITION");
impl Composition {
pub fn new(
locatable: LocatableAttrs,
category_code: &str,
composer: PartyProxy,
language: CodePhrase,
territory: CodePhrase,
) -> Result<Self, ParseError> {
let category = terminology::composition_category::GROUP
.coded_text(category_code)
.ok_or_else(|| ParseError::invariant("COMPOSITION", "Category_validity"))?;
Ok(Self {
locatable,
rm_type: CompositionTag,
language,
territory,
category,
composer,
context: None,
content: Vec::new(),
})
}
pub fn with_context(mut self, context: EventContext) -> Result<Self, ParseError> {
if self.is_persistent() {
return Err(ParseError::invariant(
"COMPOSITION",
"Is_persistent_validity",
));
}
self.context = Some(Box::new(context));
Ok(self)
}
#[must_use]
pub fn with_content(mut self, item: ContentItem) -> Self {
self.content.push(item);
self
}
#[must_use]
pub fn language(&self) -> &CodePhrase {
&self.language
}
#[must_use]
pub fn territory(&self) -> &CodePhrase {
&self.territory
}
#[must_use]
pub fn category(&self) -> &DvCodedText {
&self.category
}
#[must_use]
pub fn composer(&self) -> &PartyProxy {
&self.composer
}
#[must_use]
pub fn context(&self) -> Option<&EventContext> {
self.context.as_deref()
}
#[must_use]
pub fn content(&self) -> &[ContentItem] {
&self.content
}
#[must_use]
pub fn category_code(&self) -> &str {
self.category.defining_code().code_string()
}
#[must_use]
pub fn is_persistent(&self) -> bool {
self.category_code() == terminology::composition_category::PERSISTENT
}
#[must_use]
pub fn is_event(&self) -> bool {
self.category_code() == terminology::composition_category::EVENT
}
pub fn entries(&self) -> impl Iterator<Item = &Entry> {
self.content.iter().flat_map(ContentItem::entries)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(from = "ContentItemWire", into = "ContentItemWire")]
#[allow(clippy::large_enum_variant)]
pub enum ContentItem {
Section(Section),
Entry(Entry),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "_type")]
#[allow(clippy::large_enum_variant)]
#[doc(hidden)]
pub enum ContentItemWire {
#[serde(rename = "SECTION")]
Section(Section),
#[serde(rename = "OBSERVATION")]
Observation(Observation),
#[serde(rename = "EVALUATION")]
Evaluation(Evaluation),
#[serde(rename = "INSTRUCTION")]
Instruction(Instruction),
#[serde(rename = "ACTION")]
Action(Action),
#[serde(rename = "ADMIN_ENTRY")]
AdminEntry(AdminEntry),
}
impl From<ContentItem> for ContentItemWire {
fn from(v: ContentItem) -> Self {
match v {
ContentItem::Section(s) => Self::Section(s),
ContentItem::Entry(Entry::Observation(e)) => Self::Observation(e),
ContentItem::Entry(Entry::Evaluation(e)) => Self::Evaluation(e),
ContentItem::Entry(Entry::Instruction(e)) => Self::Instruction(e),
ContentItem::Entry(Entry::Action(e)) => Self::Action(e),
ContentItem::Entry(Entry::AdminEntry(e)) => Self::AdminEntry(e),
}
}
}
impl From<ContentItemWire> for ContentItem {
fn from(v: ContentItemWire) -> Self {
match v {
ContentItemWire::Section(s) => Self::Section(s),
ContentItemWire::Observation(e) => Self::Entry(Entry::Observation(e)),
ContentItemWire::Evaluation(e) => Self::Entry(Entry::Evaluation(e)),
ContentItemWire::Instruction(e) => Self::Entry(Entry::Instruction(e)),
ContentItemWire::Action(e) => Self::Entry(Entry::Action(e)),
ContentItemWire::AdminEntry(e) => Self::Entry(Entry::AdminEntry(e)),
}
}
}
impl ContentItem {
pub fn entries(&self) -> Box<dyn Iterator<Item = &Entry> + '_> {
match self {
Self::Entry(e) => Box::new(core::iter::once(e)),
Self::Section(s) => Box::new(s.items().iter().flat_map(ContentItem::entries)),
}
}
#[must_use]
pub fn locatable(&self) -> &LocatableAttrs {
match self {
Self::Section(s) => s.locatable(),
Self::Entry(e) => e.locatable(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Section {
#[serde(flatten)]
locatable: LocatableAttrs,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
items: Vec<ContentItem>,
}
impl_locatable!(Section, "SECTION");
impl Section {
#[must_use]
pub fn new(locatable: LocatableAttrs, items: Vec<ContentItem>) -> Self {
Self { locatable, items }
}
#[must_use]
pub fn items(&self) -> &[ContentItem] {
&self.items
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EntryAttrs {
language: CodePhrase,
encoding: CodePhrase,
subject: PartyProxy,
#[serde(skip_serializing_if = "Option::is_none", default)]
provider: Option<Box<PartyProxy>>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
other_participations: Vec<Participation>,
#[serde(skip_serializing_if = "Option::is_none", default)]
workflow_id: Option<ObjectRef>,
}
impl EntryAttrs {
#[must_use]
pub fn about_subject(language: CodePhrase, encoding: CodePhrase) -> Self {
Self {
language,
encoding,
subject: PartySelf::anonymous().into(),
provider: None,
other_participations: Vec::new(),
workflow_id: None,
}
}
#[must_use]
pub fn about(language: CodePhrase, encoding: CodePhrase, subject: PartyProxy) -> Self {
Self {
language,
encoding,
subject,
provider: None,
other_participations: Vec::new(),
workflow_id: None,
}
}
#[must_use]
pub fn with_provider(mut self, provider: PartyProxy) -> Self {
self.provider = Some(Box::new(provider));
self
}
#[must_use]
pub fn with_participation(mut self, participation: Participation) -> Self {
self.other_participations.push(participation);
self
}
#[must_use]
pub fn subject(&self) -> &PartyProxy {
&self.subject
}
#[must_use]
pub fn provider(&self) -> Option<&PartyProxy> {
self.provider.as_deref()
}
#[must_use]
pub fn language(&self) -> &CodePhrase {
&self.language
}
#[must_use]
pub fn encoding(&self) -> &CodePhrase {
&self.encoding
}
#[must_use]
pub fn other_participations(&self) -> &[Participation] {
&self.other_participations
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct CareEntryAttrs {
#[serde(skip_serializing_if = "Option::is_none", default)]
protocol: Option<Box<ItemStructure>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
guideline_id: Option<ObjectRef>,
}
impl CareEntryAttrs {
#[must_use]
pub fn with_protocol(mut self, protocol: ItemStructure) -> Self {
self.protocol = Some(Box::new(protocol));
self
}
#[must_use]
pub fn protocol(&self) -> Option<&ItemStructure> {
self.protocol.as_deref()
}
#[must_use]
pub fn guideline_id(&self) -> Option<&ObjectRef> {
self.guideline_id.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdminEntry {
#[serde(flatten)]
locatable: LocatableAttrs,
#[serde(flatten)]
entry: EntryAttrs,
data: ItemStructure,
}
impl_locatable!(AdminEntry, "ADMIN_ENTRY");
impl AdminEntry {
#[must_use]
pub fn new(locatable: LocatableAttrs, entry: EntryAttrs, data: ItemStructure) -> Self {
Self {
locatable,
entry,
data,
}
}
#[must_use]
pub fn entry(&self) -> &EntryAttrs {
&self.entry
}
#[must_use]
pub fn data(&self) -> &ItemStructure {
&self.data
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Observation {
#[serde(flatten)]
locatable: LocatableAttrs,
#[serde(flatten)]
entry: EntryAttrs,
#[serde(flatten)]
care_entry: CareEntryAttrs,
data: History,
#[serde(skip_serializing_if = "Option::is_none", default)]
state: Option<Box<History>>,
}
impl_locatable!(Observation, "OBSERVATION");
impl Observation {
#[must_use]
pub fn new(locatable: LocatableAttrs, entry: EntryAttrs, data: History) -> Self {
Self {
locatable,
entry,
care_entry: CareEntryAttrs::default(),
data,
state: None,
}
}
#[must_use]
pub fn with_state(mut self, state: History) -> Self {
self.state = Some(Box::new(state));
self
}
#[must_use]
pub fn with_care_entry(mut self, care_entry: CareEntryAttrs) -> Self {
self.care_entry = care_entry;
self
}
#[must_use]
pub fn entry(&self) -> &EntryAttrs {
&self.entry
}
#[must_use]
pub fn care_entry(&self) -> &CareEntryAttrs {
&self.care_entry
}
#[must_use]
pub fn data(&self) -> &History {
&self.data
}
#[must_use]
pub fn state(&self) -> Option<&History> {
self.state.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Evaluation {
#[serde(flatten)]
locatable: LocatableAttrs,
#[serde(flatten)]
entry: EntryAttrs,
#[serde(flatten)]
care_entry: CareEntryAttrs,
data: ItemStructure,
}
impl_locatable!(Evaluation, "EVALUATION");
impl Evaluation {
#[must_use]
pub fn new(locatable: LocatableAttrs, entry: EntryAttrs, data: ItemStructure) -> Self {
Self {
locatable,
entry,
care_entry: CareEntryAttrs::default(),
data,
}
}
#[must_use]
pub fn entry(&self) -> &EntryAttrs {
&self.entry
}
#[must_use]
pub fn data(&self) -> &ItemStructure {
&self.data
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Activity {
#[serde(rename = "_type", default)]
rm_type: ActivityTag,
#[serde(flatten)]
locatable: LocatableAttrs,
description: ItemStructure,
#[serde(skip_serializing_if = "Option::is_none", default)]
timing: Option<DvParsable>,
action_archetype_id: String,
}
impl_locatable!(Activity, "ACTIVITY");
impl Activity {
pub fn new(
locatable: LocatableAttrs,
description: ItemStructure,
action_archetype_id: impl Into<String>,
) -> Result<Self, ParseError> {
let action_archetype_id = action_archetype_id.into();
if action_archetype_id.is_empty() {
return Err(ParseError::invariant(
"ACTIVITY",
"Action_archetype_id_valid",
));
}
Ok(Self {
locatable,
description,
rm_type: ActivityTag,
timing: None,
action_archetype_id,
})
}
#[must_use]
pub fn with_timing(mut self, timing: DvParsable) -> Self {
self.timing = Some(timing);
self
}
#[must_use]
pub fn description(&self) -> &ItemStructure {
&self.description
}
#[must_use]
pub fn timing(&self) -> Option<&DvParsable> {
self.timing.as_ref()
}
#[must_use]
pub fn action_archetype_id(&self) -> &str {
&self.action_archetype_id
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Instruction {
#[serde(flatten)]
locatable: LocatableAttrs,
#[serde(flatten)]
entry: EntryAttrs,
#[serde(flatten)]
care_entry: CareEntryAttrs,
narrative: Text,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
activities: Vec<Activity>,
#[serde(skip_serializing_if = "Option::is_none", default)]
expiry_time: Option<DvDateTime>,
#[serde(skip_serializing_if = "Option::is_none", default)]
wf_definition: Option<DvParsable>,
}
impl_locatable!(Instruction, "INSTRUCTION");
impl Instruction {
pub fn new(
locatable: LocatableAttrs,
entry: EntryAttrs,
narrative: Text,
activities: Vec<Activity>,
) -> Result<Self, ParseError> {
if activities.is_empty() {
return Err(ParseError::invariant("INSTRUCTION", "Activities_valid"));
}
Ok(Self {
locatable,
entry,
care_entry: CareEntryAttrs::default(),
narrative,
activities,
expiry_time: None,
wf_definition: None,
})
}
#[must_use]
pub fn with_expiry_time(mut self, expiry_time: DvDateTime) -> Self {
self.expiry_time = Some(expiry_time);
self
}
#[must_use]
pub fn narrative(&self) -> &Text {
&self.narrative
}
#[must_use]
pub fn activities(&self) -> &[Activity] {
&self.activities
}
#[must_use]
pub fn expiry_time(&self) -> Option<&DvDateTime> {
self.expiry_time.as_ref()
}
#[must_use]
pub fn entry(&self) -> &EntryAttrs {
&self.entry
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IsmTransition {
#[serde(rename = "_type", default)]
rm_type: IsmTransitionTag,
current_state: DvCodedText,
#[serde(skip_serializing_if = "Option::is_none", default)]
transition: Option<DvCodedText>,
#[serde(skip_serializing_if = "Option::is_none", default)]
careflow_step: Option<DvCodedText>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
reason: Vec<Text>,
}
impl IsmTransition {
pub const TERMINAL_STATES: [&'static str; 4] = [
terminology::instruction_state::COMPLETED,
terminology::instruction_state::ABORTED,
terminology::instruction_state::CANCELLED,
terminology::instruction_state::EXPIRED,
];
pub fn new(current_state_code: &str) -> Result<Self, ParseError> {
let current_state = terminology::instruction_state::GROUP
.coded_text(current_state_code)
.ok_or_else(|| ParseError::invariant("ISM_TRANSITION", "Current_state_valid"))?;
Ok(Self {
current_state,
rm_type: IsmTransitionTag,
transition: None,
careflow_step: None,
reason: Vec::new(),
})
}
pub fn with_transition(mut self, transition_code: &str) -> Result<Self, ParseError> {
self.transition = Some(
terminology::instruction_transition::GROUP
.coded_text(transition_code)
.ok_or_else(|| ParseError::invariant("ISM_TRANSITION", "Transition_valid"))?,
);
Ok(self)
}
#[must_use]
pub fn with_careflow_step(mut self, careflow_step: DvCodedText) -> Self {
self.careflow_step = Some(careflow_step);
self
}
#[must_use]
pub fn with_reason(mut self, reason: Text) -> Self {
self.reason.push(reason);
self
}
#[must_use]
pub fn current_state(&self) -> &DvCodedText {
&self.current_state
}
#[must_use]
pub fn transition(&self) -> Option<&DvCodedText> {
self.transition.as_ref()
}
#[must_use]
pub fn careflow_step(&self) -> Option<&DvCodedText> {
self.careflow_step.as_ref()
}
#[must_use]
pub fn reason(&self) -> &[Text] {
&self.reason
}
#[must_use]
pub fn is_terminal(&self) -> bool {
Self::TERMINAL_STATES.contains(&self.current_state.defining_code().code_string())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InstructionDetails {
#[serde(rename = "_type", default)]
rm_type: InstructionDetailsTag,
instruction_id: LocatableRef,
activity_id: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
wf_details: Option<ItemStructure>,
}
impl InstructionDetails {
pub fn new(
instruction_id: LocatableRef,
activity_id: impl Into<String>,
) -> Result<Self, ParseError> {
let activity_id = activity_id.into();
if activity_id.is_empty() {
return Err(ParseError::invariant(
"INSTRUCTION_DETAILS",
"Activity_path_valid",
));
}
Ok(Self {
instruction_id,
rm_type: InstructionDetailsTag,
activity_id,
wf_details: None,
})
}
#[must_use]
pub fn instruction_id(&self) -> &LocatableRef {
&self.instruction_id
}
#[must_use]
pub fn activity_id(&self) -> &str {
&self.activity_id
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Action {
#[serde(flatten)]
locatable: LocatableAttrs,
#[serde(flatten)]
entry: EntryAttrs,
#[serde(flatten)]
care_entry: CareEntryAttrs,
time: DvDateTime,
description: ItemStructure,
ism_transition: IsmTransition,
#[serde(skip_serializing_if = "Option::is_none", default)]
instruction_details: Option<InstructionDetails>,
}
impl_locatable!(Action, "ACTION");
impl Action {
#[must_use]
pub fn new(
locatable: LocatableAttrs,
entry: EntryAttrs,
time: DvDateTime,
description: ItemStructure,
ism_transition: IsmTransition,
) -> Self {
Self {
locatable,
entry,
care_entry: CareEntryAttrs::default(),
time,
description,
ism_transition,
instruction_details: None,
}
}
#[must_use]
pub fn with_instruction_details(mut self, details: InstructionDetails) -> Self {
self.instruction_details = Some(details);
self
}
#[must_use]
pub fn time(&self) -> &DvDateTime {
&self.time
}
#[must_use]
pub fn description(&self) -> &ItemStructure {
&self.description
}
#[must_use]
pub fn ism_transition(&self) -> &IsmTransition {
&self.ism_transition
}
#[must_use]
pub fn instruction_details(&self) -> Option<&InstructionDetails> {
self.instruction_details.as_ref()
}
#[must_use]
pub fn entry(&self) -> &EntryAttrs {
&self.entry
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "_type")]
#[allow(clippy::large_enum_variant)]
pub enum Entry {
#[serde(rename = "OBSERVATION")]
Observation(Observation),
#[serde(rename = "EVALUATION")]
Evaluation(Evaluation),
#[serde(rename = "INSTRUCTION")]
Instruction(Instruction),
#[serde(rename = "ACTION")]
Action(Action),
#[serde(rename = "ADMIN_ENTRY")]
AdminEntry(AdminEntry),
}
impl Entry {
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::Observation(_) => "OBSERVATION",
Self::Evaluation(_) => "EVALUATION",
Self::Instruction(_) => "INSTRUCTION",
Self::Action(_) => "ACTION",
Self::AdminEntry(_) => "ADMIN_ENTRY",
}
}
#[must_use]
pub fn locatable(&self) -> &LocatableAttrs {
match self {
Self::Observation(e) => e.locatable(),
Self::Evaluation(e) => e.locatable(),
Self::Instruction(e) => e.locatable(),
Self::Action(e) => e.locatable(),
Self::AdminEntry(e) => e.locatable(),
}
}
#[must_use]
pub fn entry_attrs(&self) -> &EntryAttrs {
match self {
Self::Observation(e) => e.entry(),
Self::Evaluation(e) => e.entry(),
Self::Instruction(e) => e.entry(),
Self::Action(e) => e.entry(),
Self::AdminEntry(e) => e.entry(),
}
}
#[must_use]
pub fn is_care_entry(&self) -> bool {
!matches!(self, Self::AdminEntry(_))
}
#[must_use]
pub fn subject(&self) -> &PartyProxy {
self.entry_attrs().subject()
}
}
macro_rules! entry_from {
($($ty:ty => $variant:ident),* $(,)?) => {
$(
impl From<$ty> for Entry {
fn from(v: $ty) -> Self {
Self::$variant(v)
}
}
impl From<$ty> for ContentItem {
fn from(v: $ty) -> Self {
Self::Entry(Entry::$variant(v))
}
}
)*
};
}
entry_from! {
Observation => Observation,
Evaluation => Evaluation,
Instruction => Instruction,
Action => Action,
AdminEntry => AdminEntry,
}
impl From<Entry> for ContentItem {
fn from(v: Entry) -> Self {
Self::Entry(v)
}
}
impl From<Section> for ContentItem {
fn from(v: Section) -> Self {
Self::Section(v)
}
}
pub type Label = DvText;
#[cfg(test)]
mod tests {
use super::*;
use crate::base::PartyRef;
use crate::rm::common::{Archetyped, PartyIdentified, PartyRelated};
use crate::rm::data_structures::{Element, ItemSingle};
use crate::rm::data_types::{DataValue, DvCount};
fn attrs(name: &str, node: &str) -> LocatableAttrs {
LocatableAttrs::named(name, node).unwrap()
}
fn en() -> CodePhrase {
CodePhrase::new("ISO_639-1", "en").unwrap()
}
fn utf8() -> CodePhrase {
CodePhrase::new("IANA_character-sets", "UTF-8").unwrap()
}
fn item_structure() -> ItemStructure {
ItemSingle::new(
attrs("d", "at0001"),
Element::new(attrs("v", "at0002"), DataValue::Count(DvCount::new(1))),
)
.into()
}
fn composition() -> Composition {
Composition::new(
attrs("Encounter", "openEHR-EHR-COMPOSITION.encounter.v1").with_archetype_details(
Archetyped::new("openEHR-EHR-COMPOSITION.encounter.v1", "1.1.0").unwrap(),
),
terminology::composition_category::EVENT,
PartyIdentified::named("Dr A Nurse").unwrap().into(),
en(),
CodePhrase::new("ISO_3166-1", "GB").unwrap(),
)
.unwrap()
}
#[test]
fn entries_are_found_through_nested_sections() {
let entry: ContentItem = Evaluation::new(
attrs("Problem", "at0000"),
EntryAttrs::about_subject(en(), utf8()),
item_structure(),
)
.into();
let inner = Section::new(attrs("Inner", "at0002"), vec![entry]);
let outer = Section::new(attrs("Outer", "at0001"), vec![inner.into()]);
let c = composition().with_content(outer.into());
assert_eq!(c.entries().count(), 1);
assert_eq!(c.entries().next().unwrap().type_name(), "EVALUATION");
}
#[test]
fn an_entry_about_someone_else_says_so() {
let mother = PartyRelated::new(
PartyIdentified::named("Mother").unwrap(),
terminology::subject_relationship::MOTHER,
)
.unwrap();
let family_history = Evaluation::new(
attrs("Family history", "at0000"),
EntryAttrs::about(en(), utf8(), mother.into()),
item_structure(),
);
let entry: Entry = family_history.into();
assert!(!entry.subject().is_subject());
let own = Evaluation::new(
attrs("Problem", "at0000"),
EntryAttrs::about_subject(en(), utf8()),
item_structure(),
);
assert!(Entry::from(own).subject().is_subject());
}
#[test]
fn a_composition_round_trips_through_canonical_json() {
let c = composition().with_content(
Evaluation::new(
attrs("Problem", "at0000"),
EntryAttrs::about_subject(en(), utf8()),
item_structure(),
)
.into(),
);
let json = serde_json::to_string(&c).unwrap();
assert!(json.contains(r#""_type":"EVALUATION""#), "{json}");
let back: Composition = serde_json::from_str(&json).unwrap();
assert_eq!(back, c);
}
#[test]
fn an_instruction_needs_at_least_one_activity() {
assert!(
Instruction::new(
attrs("Order", "at0000"),
EntryAttrs::about_subject(en(), utf8()),
Text::plain("Amoxicillin 500mg three times a day").unwrap(),
Vec::new(),
)
.is_err()
);
}
#[test]
fn terminal_ism_states_are_recognised() {
for code in IsmTransition::TERMINAL_STATES {
assert!(IsmTransition::new(code).unwrap().is_terminal(), "{code}");
}
for code in [
terminology::instruction_state::ACTIVE,
terminology::instruction_state::PLANNED,
terminology::instruction_state::SUSPENDED,
] {
assert!(!IsmTransition::new(code).unwrap().is_terminal(), "{code}");
}
}
#[test]
fn an_event_context_cannot_end_before_it_starts() {
let ctx = EventContext::new(
DvDateTime::new("2026-07-31T09:00:00Z").unwrap(),
terminology::setting::PRIMARY_MEDICAL_CARE,
)
.unwrap();
assert!(
ctx.clone()
.with_end_time(DvDateTime::new("2026-07-31T08:00:00Z").unwrap())
.is_err()
);
assert!(
ctx.with_end_time(DvDateTime::new("2026-07-31T10:00:00Z").unwrap())
.is_ok()
);
}
#[test]
fn deactivating_a_record_does_not_make_it_unreadable() {
let status = EhrStatus::new(
attrs("EHR Status", "openEHR-EHR-EHR_STATUS.generic.v1"),
PartySelf::anonymous(),
true,
true,
);
let deceased = status.set_modifiable(false);
assert!(!deceased.is_active());
assert!(deceased.is_queryable());
}
#[test]
fn an_ehr_status_reports_the_flags_that_govern_the_record() {
let status = |queryable: bool, modifiable: bool| {
EhrStatus::new(
attrs("status", "openEHR-EHR-EHR_STATUS.generic.v1"),
PartySelf::anonymous(),
queryable,
modifiable,
)
};
for (queryable, modifiable) in [(true, true), (true, false), (false, true), (false, false)] {
let s = status(queryable, modifiable);
assert_eq!(s.is_queryable(), queryable, "queryable={queryable} modifiable={modifiable}");
assert_eq!(s.is_modifiable(), modifiable, "queryable={queryable} modifiable={modifiable}");
assert_eq!(s.is_active(), modifiable);
}
let anonymous = status(true, true);
assert_eq!(anonymous.subject(), &PartySelf::anonymous());
let person = PartyRef::new(
"demographic",
"PERSON",
crate::base::ObjectId::HierObjectId(
crate::base::HierObjectId::from_uid_str("6BA7B810-9DAD-11D1-80B4-00C04FD430C8")
.unwrap(),
),
)
.unwrap();
let identified = EhrStatus::new(
attrs("status", "openEHR-EHR-EHR_STATUS.generic.v1"),
PartySelf::with_external_ref(person.clone()),
true,
true,
);
assert_eq!(identified.subject().external_ref(), Some(&person));
assert_ne!(identified.subject(), anonymous.subject());
assert_eq!(anonymous.other_details(), None);
let detailed = status(true, true).with_other_details(item_structure());
assert!(detailed.other_details().is_some());
}
#[test]
fn a_composition_reports_whether_it_is_an_event_or_persistent() {
let event = composition();
assert!(event.is_event(), "an encounter is an event composition");
assert!(!event.is_persistent());
let persistent = Composition::new(
attrs("Problem list", "openEHR-EHR-COMPOSITION.problem_list.v1"),
terminology::composition_category::PERSISTENT,
PartyIdentified::named("Dr A Nurse").unwrap().into(),
en(),
CodePhrase::new("ISO_3166-1", "GB").unwrap(),
)
.unwrap();
assert!(persistent.is_persistent());
assert!(
!persistent.is_event(),
"a persistent composition was reported as an event"
);
assert_ne!(event.is_event(), persistent.is_event());
}
#[test]
fn the_optional_attributes_of_an_entry_are_reported_as_recorded() {
let bare = EntryAttrs::about_subject(en(), utf8());
assert_eq!(bare.provider(), None);
assert!(bare.other_participations().is_empty());
let provider: crate::rm::common::PartyProxy =
PartyIdentified::named("Dr A Nurse").unwrap().into();
let participation = crate::rm::common::Participation::new(
Text::Plain(crate::rm::data_types::DvText::new("witness").unwrap()),
PartyIdentified::named("Ms B Witness").unwrap().into(),
);
let full = EntryAttrs::about_subject(en(), utf8())
.with_provider(provider.clone())
.with_participation(participation);
assert_eq!(full.provider().and_then(PartyProxy::name), Some("Dr A Nurse"));
assert_eq!(full.other_participations().len(), 1);
assert_eq!(CareEntryAttrs::default().guideline_id(), None);
let activity = Activity::new(
attrs("activity", "at0400"),
item_structure(),
"openEHR-EHR-ACTION.medication.v1",
)
.unwrap();
assert_eq!(
activity.action_archetype_id(),
"openEHR-EHR-ACTION.medication.v1",
"the link from an order to its action was lost"
);
assert_eq!(activity.timing(), None);
assert!(Activity::new(attrs("activity", "at0400"), item_structure(), "").is_err());
let plain = IsmTransition::new(terminology::instruction_state::ACTIVE).unwrap();
assert_eq!(plain.transition(), None);
assert_eq!(plain.careflow_step(), None);
assert!(plain.reason().is_empty());
assert_eq!(
plain.current_state().defining_code().code_string(),
terminology::instruction_state::ACTIVE
);
let moved = IsmTransition::new(terminology::instruction_state::ACTIVE)
.unwrap()
.with_transition(terminology::instruction_transition::START)
.unwrap()
.with_careflow_step(
DvCodedText::new("dispensed", CodePhrase::new("local", "at0010").unwrap()).unwrap(),
)
.with_reason(Text::Plain(
crate::rm::data_types::DvText::new("stock available").unwrap(),
));
assert_eq!(
moved.transition().map(|t| t.defining_code().code_string()),
Some(terminology::instruction_transition::START)
);
assert_eq!(
moved.careflow_step().map(crate::rm::data_types::DvCodedText::value),
Some("dispensed")
);
assert_eq!(moved.reason().len(), 1);
assert_eq!(moved.reason()[0].value(), "stock available");
assert!(
IsmTransition::new(terminology::instruction_state::ACTIVE)
.unwrap()
.with_transition("not-a-transition")
.is_err()
);
let timed = Activity::new(
attrs("activity", "at0400"),
item_structure(),
"openEHR-EHR-ACTION.medication.v1",
)
.unwrap()
.with_timing(crate::rm::data_types::DvParsable::new("R2/2026-08-03/P1D", "ISO8601").unwrap());
assert_eq!(
timed.timing().map(crate::rm::data_types::DvParsable::value),
Some("R2/2026-08-03/P1D")
);
let with_guideline: CareEntryAttrs = serde_json::from_str(
r#"{"guideline_id":{"namespace":"local","type":"GUIDELINE",
"id":{"_type":"HIER_OBJECT_ID","value":"6BA7B810-9DAD-11D1-80B4-00C04FD430C8"}}}"#,
)
.expect("deserialize");
assert!(
with_guideline.guideline_id().is_some(),
"a recorded guideline was dropped"
);
}
#[test]
fn a_folder_and_an_event_context_report_what_they_hold() {
let doc = |n: u32| {
ObjectRef::new(
"local",
"VERSIONED_COMPOSITION",
crate::base::ObjectId::HierObjectId(
crate::base::HierObjectId::from_uid_str(&format!(
"6BA7B810-9DAD-11D1-80B4-00C04FD430C{n:X}"
))
.unwrap(),
),
)
.unwrap()
};
let empty = Folder::new(attrs("root", "at0000"));
assert!(empty.items().is_empty());
assert!(empty.folders().is_empty());
assert_eq!(empty.details(), None);
let filed = Folder::new(attrs("root", "at0000"))
.with_item(doc(1))
.with_item(doc(2))
.with_folder(Folder::new(attrs("2026", "at0001")).with_item(doc(3)));
assert_eq!(filed.items().len(), 2, "filed documents were reported missing");
assert_eq!(filed.folders().len(), 1);
assert_eq!(filed.folders()[0].items().len(), 1, "a sub-folder was reported empty");
let mut object = serde_json::to_value(&filed)
.expect("serialize")
.as_object()
.expect("an object")
.clone();
object.insert(
"details".to_owned(),
serde_json::to_value(item_structure()).expect("serialize"),
);
let revived: Folder =
serde_json::from_value(serde_json::Value::Object(object)).expect("deserialize");
assert!(revived.details().is_some(), "recorded folder details were dropped");
assert_eq!(revived.items().len(), 2);
let open = EventContext::new(
DvDateTime::new("2026-08-03T09:00:00Z").unwrap(),
terminology::setting::EMERGENCY_CARE,
)
.unwrap();
assert_eq!(open.end_time(), None);
assert_eq!(open.other_context(), None);
let closed = EventContext::new(
DvDateTime::new("2026-08-03T09:00:00Z").unwrap(),
terminology::setting::EMERGENCY_CARE,
)
.unwrap()
.with_end_time(DvDateTime::new("2026-08-03T10:30:00Z").unwrap())
.unwrap()
.with_other_context(item_structure());
assert_eq!(
closed.end_time().map(DvDateTime::as_str),
Some("2026-08-03T10:30:00Z")
);
assert!(closed.other_context().is_some());
assert!(
EventContext::new(
DvDateTime::new("2026-08-03T09:00:00Z").unwrap(),
terminology::setting::EMERGENCY_CARE,
)
.unwrap()
.with_end_time(DvDateTime::new("2026-08-03T08:00:00Z").unwrap())
.is_err()
);
}
#[test]
fn an_instruction_and_an_action_report_the_links_between_them() {
let activity = Activity::new(
attrs("activity", "at0400"),
item_structure(),
"openEHR-EHR-ACTION.medication.v1",
)
.unwrap();
let instruction = Instruction::new(
attrs("Order", "openEHR-EHR-INSTRUCTION.medication_order.v3"),
EntryAttrs::about_subject(en(), utf8()),
Text::Plain(crate::rm::data_types::DvText::new("Give 5mg at once").unwrap()),
vec![activity],
)
.unwrap();
assert_eq!(instruction.expiry_time(), None);
let expiring = Instruction::new(
attrs("Order", "openEHR-EHR-INSTRUCTION.medication_order.v3"),
EntryAttrs::about_subject(en(), utf8()),
Text::Plain(crate::rm::data_types::DvText::new("Give 5mg at once").unwrap()),
vec![Activity::new(
attrs("activity", "at0400"),
item_structure(),
"openEHR-EHR-ACTION.medication.v1",
)
.unwrap()],
)
.unwrap()
.with_expiry_time(DvDateTime::new("2026-08-10T09:00:00Z").unwrap());
assert_eq!(
expiring.expiry_time().map(DvDateTime::as_str),
Some("2026-08-10T09:00:00Z")
);
let action = || {
Action::new(
attrs("Given", "openEHR-EHR-ACTION.medication.v1"),
EntryAttrs::about_subject(en(), utf8()),
DvDateTime::new("2026-08-03T10:00:00Z").unwrap(),
item_structure(),
IsmTransition::new(terminology::instruction_state::ACTIVE).unwrap(),
)
};
assert_eq!(action().instruction_details(), None);
let details = InstructionDetails::new(
LocatableRef::new(
"local",
"VERSIONED_COMPOSITION",
crate::base::UidBasedId::from(
"6BA7B810-9DAD-11D1-80B4-00C04FD430C8"
.parse::<crate::base::HierObjectId>()
.unwrap(),
),
Some("/content[at0001]".to_owned()),
)
.unwrap(),
"activity1",
)
.unwrap();
let linked = action().with_instruction_details(details);
assert!(
linked.instruction_details().is_some(),
"the link back to the order was lost"
);
assert_eq!(
linked.instruction_details().map(InstructionDetails::activity_id),
Some("activity1")
);
}
}