use std::fmt;
use secrecy::{ExposeSecret, SecretString};
use url::Url;
use super::error::InvalidInput;
use ihe_atna::redact::REDACTED;
#[derive(Clone)]
pub struct SourceIdentifier {
system: String,
value: SecretString,
}
impl SourceIdentifier {
pub fn new(system: impl Into<String>, value: SecretString) -> Result<Self, InvalidInput> {
let system = absolute_uri(system.into())?;
if value.expose_secret().is_empty() {
return Err(InvalidInput::EmptyValue);
}
Ok(Self { system, value })
}
#[must_use]
pub fn system(&self) -> &str {
&self.system
}
#[must_use]
pub fn value(&self) -> &SecretString {
&self.value
}
}
impl fmt::Debug for SourceIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SourceIdentifier")
.field("system", &self.system)
.field("value", &REDACTED)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TargetSystem(String);
impl TargetSystem {
pub fn new(system: impl Into<String>) -> Result<Self, InvalidInput> {
Ok(Self(absolute_uri(system.into())?))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone)]
pub struct TargetIdentifier {
system: String,
value: SecretString,
}
impl TargetIdentifier {
pub(super) fn new(system: String, value: SecretString) -> Self {
Self { system, value }
}
#[must_use]
pub fn system(&self) -> &str {
&self.system
}
#[must_use]
pub fn value(&self) -> &SecretString {
&self.value
}
}
impl fmt::Debug for TargetIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TargetIdentifier")
.field("system", &self.system)
.field("value", &REDACTED)
.finish()
}
}
#[derive(Clone)]
pub struct PatientReference(SecretString);
impl PatientReference {
pub(super) fn new(reference: SecretString) -> Self {
Self(reference)
}
#[must_use]
pub fn reference(&self) -> &SecretString {
&self.0
}
}
impl fmt::Debug for PatientReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PatientReference({REDACTED})")
}
}
#[derive(Debug, Clone, Default)]
pub struct CrossReferences {
identifiers: Vec<TargetIdentifier>,
patients: Vec<PatientReference>,
}
impl CrossReferences {
pub(super) fn new(identifiers: Vec<TargetIdentifier>, patients: Vec<PatientReference>) -> Self {
Self {
identifiers,
patients,
}
}
#[must_use]
pub fn identifiers(&self) -> &[TargetIdentifier] {
&self.identifiers
}
#[must_use]
pub fn patients(&self) -> &[PatientReference] {
&self.patients
}
pub fn in_domain<'a>(&'a self, system: &'a str) -> impl Iterator<Item = &'a TargetIdentifier> {
self.identifiers
.iter()
.filter(move |identifier| identifier.system == system)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum CrossReference {
Matched(CrossReferences),
SourceNotFound,
}
fn absolute_uri(text: String) -> Result<String, InvalidInput> {
if text.is_empty() || text.chars().any(char::is_whitespace) || Url::parse(&text).is_err() {
return Err(InvalidInput::System);
}
Ok(text)
}
#[cfg(test)]
mod tests {
use secrecy::SecretString;
use super::{
CrossReferences, InvalidInput, PatientReference, SourceIdentifier, TargetIdentifier,
TargetSystem,
};
use ihe_atna::redact::REDACTED;
const SENTINEL: &str = "SENTINEL-4711";
#[test]
fn debug_never_shows_an_identifier_value() {
let source = SourceIdentifier::new("urn:oid:2.999.1", SecretString::from(SENTINEL))
.expect("a valid source identifier");
let found = CrossReferences::new(
vec![TargetIdentifier::new(
"urn:oid:2.999.2".into(),
SecretString::from(SENTINEL),
)],
vec![PatientReference::new(SecretString::from(format!(
"Patient/{SENTINEL}"
)))],
);
for shown in [format!("{source:?}"), format!("{found:?}")] {
assert!(
!shown.contains(SENTINEL),
"Debug showed an identifier value"
);
assert!(shown.contains(REDACTED), "the placeholder shows: {shown}");
}
assert!(
format!("{found:?}").contains("PatientReference(***)"),
"a reference shows the family's placeholder"
);
assert!(
format!("{source:?}").contains("urn:oid:2.999.1"),
"the assigning authority is no patient data and prints"
);
}
#[test]
fn a_system_must_be_an_absolute_uri() {
for bad in ["", "not a uri", "relative/path", "2.999.1"] {
assert_eq!(
SourceIdentifier::new(bad, SecretString::from("1")).err(),
Some(InvalidInput::System),
"{bad:?} is no assigning authority"
);
assert_eq!(
TargetSystem::new(bad).err(),
Some(InvalidInput::System),
"{bad:?} is no target system"
);
}
assert!(
TargetSystem::new("urn:oid:2.999.1").is_ok(),
"an OID URN is an absolute URI"
);
}
#[test]
fn an_empty_value_is_refused() {
assert_eq!(
SourceIdentifier::new("urn:oid:2.999.1", SecretString::from("")).err(),
Some(InvalidInput::EmptyValue),
"an empty identifier names no patient"
);
}
}