use crate::{GeneratedFrom as From_, MwlError, UidPolicy, Warning};
use hl7kit::order::{Order, StudyUidSource};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StudyUidOrigin {
FromOrder(StudyUidSource),
Generated(GeneratedFrom),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeneratedFrom {
RequestedProcedureId,
AccessionNumber,
Random,
}
impl std::fmt::Display for GeneratedFrom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GeneratedFrom::RequestedProcedureId => write!(f, "from the Requested Procedure ID"),
GeneratedFrom::AccessionNumber => write!(f, "from the Accession Number"),
GeneratedFrom::Random => write!(f, "at random"),
}
}
}
pub const NAMESPACE: &str = "mwlkit.study-instance-uid";
pub fn uuid_uid(bits: u128) -> String {
format!("2.25.{bits}")
}
pub fn name_based_uid(namespace: &str, name: &str) -> String {
let text = format!("{namespace}|{name}");
let hi = fnv1a(text.as_bytes(), 0xcbf2_9ce4_8422_2325);
let lo = fnv1a(text.as_bytes(), 0x84222325_cbf29ce4 ^ 0x5bd1_e995_9a2b_f347);
let hi = (hi & 0xffff_ffff_ffff_0fff) | 0x0000_0000_0000_8000; let lo = (lo & 0x3fff_ffff_ffff_ffff) | 0x8000_0000_0000_0000; uuid_uid((u128::from(hi) << 64) | u128::from(lo))
}
fn fnv1a(bytes: &[u8], offset: u64) -> u64 {
let mut h = offset;
for &b in bytes {
h ^= u64::from(b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
pub fn resolve(
order: &Order,
policy: &UidPolicy,
) -> Result<(String, StudyUidOrigin, Option<Warning>), MwlError> {
if let (Some(uid), Some(source)) = (&order.study_uid, order.study_uid_source) {
return Ok((uid.clone(), StudyUidOrigin::FromOrder(source), None));
}
let (uid, from) = match policy {
UidPolicy::Refuse => return Err(MwlError::NoStudyUid),
UidPolicy::Random(bits) => (uuid_uid(*bits), From_::Random),
UidPolicy::Dcm4cheStyle => match (&order.procedure_id, &order.accession) {
(Some(rp), _) => (
name_based_uid(NAMESPACE, &format!("rp|{rp}")),
From_::RequestedProcedureId,
),
(None, Some(acc)) => (
name_based_uid(NAMESPACE, &format!("acc|{acc}")),
From_::AccessionNumber,
),
(None, None) => return Err(MwlError::NoStudyUid),
},
};
Ok((
uid,
StudyUidOrigin::Generated(from),
Some(Warning::StudyUidGenerated(from)),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_uids_are_2_25_decimal_and_short_enough() {
let u = name_based_uid(NAMESPACE, "rp|RP-1");
assert!(u.starts_with("2.25."));
assert!(u[5..].chars().all(|c| c.is_ascii_digit()));
assert!(u.len() <= 44, "{u} is {} chars", u.len());
assert_eq!(u, name_based_uid(NAMESPACE, "rp|RP-1"), "deterministic");
assert_ne!(u, name_based_uid(NAMESPACE, "rp|RP-2"));
assert_ne!(u, name_based_uid("other", "rp|RP-1"));
assert_eq!(uuid_uid(0), "2.25.0");
assert_eq!(uuid_uid(u128::MAX).len(), 44);
}
#[test]
fn policies() {
let mut order = Order::default();
assert_eq!(
resolve(&order, &UidPolicy::Refuse),
Err(MwlError::NoStudyUid)
);
assert_eq!(
resolve(&order, &UidPolicy::Dcm4cheStyle),
Err(MwlError::NoStudyUid)
);
let (u, origin, w) = resolve(&order, &UidPolicy::Random(7)).unwrap();
assert_eq!(u, "2.25.7");
assert_eq!(origin, StudyUidOrigin::Generated(GeneratedFrom::Random));
assert_eq!(w, Some(Warning::StudyUidGenerated(GeneratedFrom::Random)));
order.accession = Some("ACC-1".into());
let (a, origin, _) = resolve(&order, &UidPolicy::Dcm4cheStyle).unwrap();
assert_eq!(
origin,
StudyUidOrigin::Generated(GeneratedFrom::AccessionNumber)
);
order.procedure_id = Some("RP-1".into());
let (r, origin, _) = resolve(&order, &UidPolicy::Dcm4cheStyle).unwrap();
assert_eq!(
origin,
StudyUidOrigin::Generated(GeneratedFrom::RequestedProcedureId)
);
assert_ne!(a, r);
order.study_uid = Some("1.2.3".into());
order.study_uid_source = Some(StudyUidSource::Ipc3);
let (u, origin, w) = resolve(&order, &UidPolicy::Refuse).unwrap();
assert_eq!(u, "1.2.3");
assert_eq!(origin, StudyUidOrigin::FromOrder(StudyUidSource::Ipc3));
assert_eq!(w, None);
}
}