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, 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 {
#[must_use]
pub fn new(
system_id: HierObjectId,
ehr_id: HierObjectId,
ehr_status: ObjectRef,
ehr_access: ObjectRef,
time_created: DvDateTime,
) -> Self {
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.partial_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::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());
}
}