1use crate::{GeneratedFrom as From_, MwlError, UidPolicy, Warning};
13use hl7kit::order::{Order, StudyUidSource};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum StudyUidOrigin {
18 FromOrder(StudyUidSource),
20 Generated(GeneratedFrom),
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum GeneratedFrom {
27 RequestedProcedureId,
29 AccessionNumber,
31 Random,
33}
34
35impl std::fmt::Display for GeneratedFrom {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 GeneratedFrom::RequestedProcedureId => write!(f, "from the Requested Procedure ID"),
39 GeneratedFrom::AccessionNumber => write!(f, "from the Accession Number"),
40 GeneratedFrom::Random => write!(f, "at random"),
41 }
42 }
43}
44
45pub const NAMESPACE: &str = "mwlkit.study-instance-uid";
49
50pub fn uuid_uid(bits: u128) -> String {
52 format!("2.25.{bits}")
53}
54
55pub fn name_based_uid(namespace: &str, name: &str) -> String {
58 let text = format!("{namespace}|{name}");
59 let hi = fnv1a(text.as_bytes(), 0xcbf2_9ce4_8422_2325);
60 let lo = fnv1a(text.as_bytes(), 0x84222325_cbf29ce4 ^ 0x5bd1_e995_9a2b_f347);
61 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))
64}
65
66fn fnv1a(bytes: &[u8], offset: u64) -> u64 {
67 let mut h = offset;
68 for &b in bytes {
69 h ^= u64::from(b);
70 h = h.wrapping_mul(0x0000_0100_0000_01b3);
71 }
72 h
73}
74
75pub fn resolve(
77 order: &Order,
78 policy: &UidPolicy,
79) -> Result<(String, StudyUidOrigin, Option<Warning>), MwlError> {
80 if let (Some(uid), Some(source)) = (&order.study_uid, order.study_uid_source) {
81 return Ok((uid.clone(), StudyUidOrigin::FromOrder(source), None));
82 }
83 let (uid, from) = match policy {
84 UidPolicy::Refuse => return Err(MwlError::NoStudyUid),
85 UidPolicy::Random(bits) => (uuid_uid(*bits), From_::Random),
86 UidPolicy::Dcm4cheStyle => match (&order.procedure_id, &order.accession) {
89 (Some(rp), _) => (
90 name_based_uid(NAMESPACE, &format!("rp|{rp}")),
91 From_::RequestedProcedureId,
92 ),
93 (None, Some(acc)) => (
94 name_based_uid(NAMESPACE, &format!("acc|{acc}")),
95 From_::AccessionNumber,
96 ),
97 (None, None) => return Err(MwlError::NoStudyUid),
98 },
99 };
100 Ok((
101 uid,
102 StudyUidOrigin::Generated(from),
103 Some(Warning::StudyUidGenerated(from)),
104 ))
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn generated_uids_are_2_25_decimal_and_short_enough() {
113 let u = name_based_uid(NAMESPACE, "rp|RP-1");
114 assert!(u.starts_with("2.25."));
115 assert!(u[5..].chars().all(|c| c.is_ascii_digit()));
116 assert!(u.len() <= 44, "{u} is {} chars", u.len());
117 assert_eq!(u, name_based_uid(NAMESPACE, "rp|RP-1"), "deterministic");
118 assert_ne!(u, name_based_uid(NAMESPACE, "rp|RP-2"));
119 assert_ne!(u, name_based_uid("other", "rp|RP-1"));
120 assert_eq!(uuid_uid(0), "2.25.0");
121 assert_eq!(uuid_uid(u128::MAX).len(), 44);
122 }
123
124 #[test]
125 fn policies() {
126 let mut order = Order::default();
127 assert_eq!(
128 resolve(&order, &UidPolicy::Refuse),
129 Err(MwlError::NoStudyUid)
130 );
131 assert_eq!(
132 resolve(&order, &UidPolicy::Dcm4cheStyle),
133 Err(MwlError::NoStudyUid)
134 );
135 let (u, origin, w) = resolve(&order, &UidPolicy::Random(7)).unwrap();
136 assert_eq!(u, "2.25.7");
137 assert_eq!(origin, StudyUidOrigin::Generated(GeneratedFrom::Random));
138 assert_eq!(w, Some(Warning::StudyUidGenerated(GeneratedFrom::Random)));
139
140 order.accession = Some("ACC-1".into());
141 let (a, origin, _) = resolve(&order, &UidPolicy::Dcm4cheStyle).unwrap();
142 assert_eq!(
143 origin,
144 StudyUidOrigin::Generated(GeneratedFrom::AccessionNumber)
145 );
146 order.procedure_id = Some("RP-1".into());
147 let (r, origin, _) = resolve(&order, &UidPolicy::Dcm4cheStyle).unwrap();
148 assert_eq!(
149 origin,
150 StudyUidOrigin::Generated(GeneratedFrom::RequestedProcedureId)
151 );
152 assert_ne!(a, r);
153
154 order.study_uid = Some("1.2.3".into());
155 order.study_uid_source = Some(StudyUidSource::Ipc3);
156 let (u, origin, w) = resolve(&order, &UidPolicy::Refuse).unwrap();
157 assert_eq!(u, "1.2.3");
158 assert_eq!(origin, StudyUidOrigin::FromOrder(StudyUidSource::Ipc3));
159 assert_eq!(w, None);
160 }
161}