use std::fmt;
use jiff::Timestamp;
use quick_xml::Writer;
use quick_xml::events::{BytesDecl, BytesText, Event};
use secrecy::{ExposeSecret as _, SecretString};
use url::Url;
use uuid::Uuid;
use super::error::{InvalidInput, XcpdError};
use super::identifier::{HomeCommunityId, Oid, PatientIdentifier};
use super::security::XuaAssertion;
use super::{ACTION, HL7, SOAP, WSA, WSSE};
use ihe_atna::redact::RedactedUrl;
const HL7_INTERACTION: &str = "2.16.840.1.113883.1.6";
pub(super) const ANONYMOUS: &str = "http://www.w3.org/2005/08/addressing/anonymous";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ProcessingCode {
#[default]
Production,
Training,
Debugging,
}
impl ProcessingCode {
fn code(self) -> &'static str {
match self {
Self::Production => "P",
Self::Training => "T",
Self::Debugging => "D",
}
}
}
#[derive(Clone)]
pub struct RespondingGateway {
endpoint: Url,
device: Oid,
community: Option<HomeCommunityId>,
}
impl RespondingGateway {
pub fn new(endpoint: Url, device: Oid) -> Result<Self, InvalidInput> {
if endpoint.scheme() != "https" {
return Err(InvalidInput::Endpoint);
}
Self::any_scheme(endpoint, device)
}
pub fn unencrypted_for_development(endpoint: Url, device: Oid) -> Result<Self, InvalidInput> {
Self::any_scheme(endpoint, device)
}
fn any_scheme(endpoint: Url, device: Oid) -> Result<Self, InvalidInput> {
if !matches!(endpoint.scheme(), "http" | "https") || endpoint.fragment().is_some() {
return Err(InvalidInput::Endpoint);
}
Ok(Self {
endpoint,
device,
community: None,
})
}
#[must_use]
pub fn targeting(mut self, community: HomeCommunityId) -> Self {
self.community = Some(community);
self
}
#[must_use]
pub fn endpoint(&self) -> &Url {
&self.endpoint
}
#[must_use]
pub fn device(&self) -> &Oid {
&self.device
}
}
impl fmt::Debug for RespondingGateway {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RespondingGateway")
.field("endpoint", &RedactedUrl(self.endpoint.as_str()))
.field("device", &self.device)
.field("community", &self.community)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct DiscoveryQuery {
sender: Oid,
community: Option<HomeCommunityId>,
patient: PatientIdentifier,
processing: ProcessingCode,
}
impl DiscoveryQuery {
#[must_use]
pub fn new(sender: Oid, patient: PatientIdentifier) -> Self {
Self {
sender,
community: None,
patient,
processing: ProcessingCode::default(),
}
}
#[must_use]
pub fn sent_for(mut self, community: HomeCommunityId) -> Self {
self.community = Some(community);
self
}
#[must_use]
pub fn processing(mut self, processing: ProcessingCode) -> Self {
self.processing = processing;
self
}
#[must_use]
pub fn patient(&self) -> &PatientIdentifier {
&self.patient
}
#[must_use]
pub fn community(&self) -> Option<&HomeCommunityId> {
self.community.as_ref()
}
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Ids {
pub(super) message: Uuid,
pub(super) query: Uuid,
pub(super) created: Timestamp,
}
impl Ids {
pub(super) fn fresh() -> Self {
Self {
message: Uuid::new_v4(),
query: Uuid::new_v4(),
created: Timestamp::now(),
}
}
pub(super) fn message_urn(&self) -> String {
format!("urn:uuid:{}", self.message)
}
}
pub(super) fn envelope(
query: &DiscoveryQuery,
gateway: &RespondingGateway,
ids: &Ids,
assertion: Option<&XuaAssertion>,
) -> Result<Written, XcpdError> {
let segment = query_by_parameter(query, ids).map_err(XcpdError::Encode)?;
let mut writer = Writer::new(Vec::new());
writer
.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
.map_err(XcpdError::Encode)?;
writer
.create_element("soap:Envelope")
.with_attributes([("xmlns:soap", SOAP), ("xmlns:wsa", WSA)])
.write_inner_content(|writer| {
writer
.create_element("soap:Header")
.write_inner_content(|writer| header(writer, gateway, ids, assertion))?;
writer
.create_element("soap:Body")
.write_inner_content(|writer| message(writer, query, gateway, ids, &segment))?;
Ok(())
})
.map_err(XcpdError::Encode)?;
Ok(Written {
body: writer.into_inner(),
query: SecretString::from(segment),
})
}
pub(super) struct Written {
pub(super) body: Vec<u8>,
pub(super) query: SecretString,
}
type Out = Writer<Vec<u8>>;
fn header(
writer: &mut Out,
gateway: &RespondingGateway,
ids: &Ids,
assertion: Option<&XuaAssertion>,
) -> std::io::Result<()> {
writer
.create_element("wsa:Action")
.with_attribute(("soap:mustUnderstand", "1"))
.write_text_content(BytesText::new(ACTION))?;
writer
.create_element("wsa:MessageID")
.write_text_content(BytesText::new(&ids.message_urn()))?;
writer
.create_element("wsa:ReplyTo")
.write_inner_content(|writer| {
writer
.create_element("wsa:Address")
.write_text_content(BytesText::new(ANONYMOUS))?;
Ok(())
})?;
writer
.create_element("wsa:To")
.write_text_content(BytesText::new(gateway.endpoint.as_str()))?;
if let Some(assertion) = assertion {
writer
.create_element("wsse:Security")
.with_attributes([("xmlns:wsse", WSSE), ("soap:mustUnderstand", "1")])
.write_inner_content(|writer| {
writer.write_event(Event::Text(BytesText::from_escaped(assertion.as_str())))
})?;
}
Ok(())
}
fn message(
writer: &mut Out,
query: &DiscoveryQuery,
gateway: &RespondingGateway,
ids: &Ids,
segment: &str,
) -> std::io::Result<()> {
let message_id = ids.message.to_string();
let created = ids.created.strftime("%Y%m%d%H%M%S+0000").to_string();
writer
.create_element("PRPA_IN201305UV02")
.with_attributes([("xmlns", HL7), ("ITSVersion", "XML_1.0")])
.write_inner_content(|writer| {
writer
.create_element("id")
.with_attribute(("root", message_id.as_str()))
.write_empty()?;
writer
.create_element("creationTime")
.with_attribute(("value", created.as_str()))
.write_empty()?;
writer
.create_element("interactionId")
.with_attributes([
("root", HL7_INTERACTION),
("extension", "PRPA_IN201305UV02"),
])
.write_empty()?;
code(writer, "processingCode", query.processing.code())?;
code(writer, "processingModeCode", "T")?;
code(writer, "acceptAckCode", "AL")?;
device(
writer,
("receiver", "RCV"),
&gateway.device,
gateway.community.as_ref(),
)?;
device(
writer,
("sender", "SND"),
&query.sender,
query.community.as_ref(),
)?;
control_act(writer, segment)?;
Ok(())
})?;
Ok(())
}
fn control_act(writer: &mut Out, segment: &str) -> std::io::Result<()> {
writer
.create_element("controlActProcess")
.with_attributes([("classCode", "CACT"), ("moodCode", "EVN")])
.write_inner_content(|writer| {
writer
.create_element("code")
.with_attributes([
("code", "PRPA_TE201305UV02"),
("codeSystem", HL7_INTERACTION),
])
.write_empty()?;
writer.write_event(Event::Text(BytesText::from_escaped(segment)))?;
Ok(())
})?;
Ok(())
}
fn query_by_parameter(query: &DiscoveryQuery, ids: &Ids) -> std::io::Result<String> {
let query_id = ids.query.to_string();
let patient = &query.patient;
let mut out = Writer::new(Vec::new());
{
let writer = &mut out;
writer
.create_element("queryByParameter")
.write_inner_content(|writer| {
writer
.create_element("queryId")
.with_attribute(("root", query_id.as_str()))
.write_empty()?;
code(writer, "statusCode", "new")?;
code(writer, "responseModalityCode", "R")?;
code(writer, "responsePriorityCode", "I")?;
writer
.create_element("parameterList")
.write_inner_content(|writer| {
writer
.create_element("livingSubjectId")
.write_inner_content(|writer| {
writer
.create_element("value")
.with_attributes([
("root", patient.authority().as_str()),
("extension", patient.value().expose_secret()),
])
.write_empty()?;
writer
.create_element("semanticsText")
.write_text_content(BytesText::new("LivingSubject.id"))?;
Ok(())
})?;
Ok(())
})?;
Ok(())
})?;
}
String::from_utf8(out.into_inner()).map_err(std::io::Error::other)
}
fn code(writer: &mut Out, name: &str, value: &str) -> std::io::Result<()> {
writer
.create_element(name)
.with_attribute(("code", value))
.write_empty()?;
Ok(())
}
fn device(
writer: &mut Out,
(role, type_code): (&str, &str),
device: &Oid,
community: Option<&HomeCommunityId>,
) -> std::io::Result<()> {
writer
.create_element(role)
.with_attribute(("typeCode", type_code))
.write_inner_content(|writer| {
writer
.create_element("device")
.with_attributes([("classCode", "DEV"), ("determinerCode", "INSTANCE")])
.write_inner_content(|writer| {
writer
.create_element("id")
.with_attribute(("root", device.as_str()))
.write_empty()?;
if let Some(community) = community {
writer
.create_element("asAgent")
.with_attribute(("classCode", "AGNT"))
.write_inner_content(|writer| {
writer
.create_element("representedOrganization")
.with_attributes([
("classCode", "ORG"),
("determinerCode", "INSTANCE"),
])
.write_inner_content(|writer| {
writer
.create_element("id")
.with_attribute(("root", community.oid().as_str()))
.write_empty()?;
Ok(())
})?;
Ok(())
})?;
}
Ok(())
})?;
Ok(())
})?;
Ok(())
}