dicom_anonymization/actions/
mod.rs

1mod empty;
2pub(crate) mod errors;
3pub mod hash;
4mod hash_date;
5mod hash_uid;
6mod keep;
7mod no_action;
8mod remove;
9mod replace;
10mod utils;
11
12use crate::actions::errors::ActionError;
13use crate::actions::hash::HASH_LENGTH_MINIMUM;
14use crate::config::Config;
15use crate::Tag;
16use dicom_object::mem::InMemElement;
17use dicom_object::DefaultDicomObject;
18use empty::Empty;
19use garde::Validate;
20use hash::{Hash, HashLength};
21use hash_date::HashDate;
22use hash_uid::HashUID;
23use keep::Keep;
24use no_action::NoAction;
25use remove::Remove;
26use replace::Replace;
27use serde::{Deserialize, Deserializer, Serialize, Serializer};
28use std::borrow::Cow;
29
30pub(crate) trait DataElementAction {
31    fn process<'a>(
32        &'a self,
33        config: &Config,
34        obj: &DefaultDicomObject,
35        elem: &'a InMemElement,
36    ) -> Result<Option<Cow<'a, InMemElement>>, ActionError>;
37}
38
39#[derive(Debug, Clone)]
40pub struct TagString(pub Tag);
41
42impl Serialize for TagString {
43    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44    where
45        S: Serializer,
46    {
47        let tag_str = format!("{}", self.0);
48        serializer.serialize_str(&tag_str)
49    }
50}
51
52impl<'de> Deserialize<'de> for TagString {
53    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54    where
55        D: Deserializer<'de>,
56    {
57        let tag_str = String::deserialize(deserializer)?;
58
59        let tag: Tag = tag_str.parse().map_err(|_| {
60            serde::de::Error::custom(format!(
61                "Tag must be in format 'GGGGEEEE' where G and E are hex digits, got: {}",
62                tag_str
63            ))
64        })?;
65
66        Ok(TagString(tag))
67    }
68}
69
70/// Specifies the action to perform on DICOM data elements during processing.
71#[derive(Validate, Serialize, Deserialize, Debug, Clone, PartialEq)]
72#[serde(tag = "action", rename_all = "lowercase")]
73pub enum Action {
74    /// Clear the value of the data element.
75    Empty,
76
77    /// Hash the data element value using an optional custom hash length.
78    Hash {
79        #[serde(skip_serializing_if = "Option::is_none")]
80        #[garde(inner(range(min = HASH_LENGTH_MINIMUM)))]
81        length: Option<usize>,
82    },
83
84    /// Change a date, using a hash of the PatientID value to determine the offset.
85    HashDate,
86
87    /// Generate a new unique identifier (UID) by hashing the original UID.
88    HashUID,
89
90    /// Preserve the original data element value without modification.
91    Keep,
92
93    /// No action specified.
94    None,
95
96    /// Completely remove the data element from the DICOM dataset.
97    Remove,
98
99    /// Replace the data element value with the specified string.
100    Replace {
101        #[garde(skip)]
102        value: String,
103    },
104}
105
106impl Action {
107    pub(crate) fn get_action_struct(&self) -> Box<dyn DataElementAction> {
108        match self {
109            Action::Empty => Box::new(Empty),
110            Action::Hash { length } => {
111                let hash_length = length.as_ref().map(|length| HashLength(*length));
112                Box::new(Hash::new(hash_length))
113            }
114            Action::HashDate => Box::new(HashDate::default()),
115            Action::HashUID => Box::new(HashUID),
116            Action::Keep => Box::new(Keep),
117            Action::None => Box::new(NoAction),
118            Action::Remove => Box::new(Remove),
119            Action::Replace { value } => Box::new(Replace::new(value.clone())),
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::Action;
127    use serde_json;
128
129    #[test]
130    fn test_serialize_empty() {
131        let action = Action::Empty;
132        let json = serde_json::to_string(&action).unwrap();
133        assert_eq!(json, r#"{"action":"empty"}"#);
134    }
135
136    #[test]
137    fn test_serialize_hash() {
138        let action = Action::Hash { length: Some(10) };
139        let json = serde_json::to_string(&action).unwrap();
140        assert_eq!(json, r#"{"action":"hash","length":10}"#);
141    }
142
143    #[test]
144    fn test_serialize_hash_date() {
145        let action = Action::HashDate;
146        let json = serde_json::to_string(&action).unwrap();
147        assert_eq!(json, r#"{"action":"hashdate"}"#);
148    }
149
150    #[test]
151    fn test_serialize_hash_uid() {
152        let action = Action::HashUID;
153        let json = serde_json::to_string(&action).unwrap();
154        assert_eq!(json, r#"{"action":"hashuid"}"#);
155    }
156
157    #[test]
158    fn test_serialize_keep() {
159        let action = Action::Keep;
160        let json = serde_json::to_string(&action).unwrap();
161        assert_eq!(json, r#"{"action":"keep"}"#);
162    }
163
164    #[test]
165    fn test_serialize_none() {
166        let action = Action::None;
167        let json = serde_json::to_string(&action).unwrap();
168        assert_eq!(json, r#"{"action":"none"}"#);
169    }
170
171    #[test]
172    fn test_serialize_remove() {
173        let action = Action::Remove;
174        let json = serde_json::to_string(&action).unwrap();
175        assert_eq!(json, r#"{"action":"remove"}"#);
176    }
177
178    #[test]
179    fn test_serialize_replace() {
180        let action = Action::Replace {
181            value: "ANONYMIZED".to_string(),
182        };
183        let json = serde_json::to_string(&action).unwrap();
184        assert_eq!(json, r#"{"action":"replace","value":"ANONYMIZED"}"#);
185    }
186
187    #[test]
188    fn test_deserialize_empty() {
189        let json = r#"{"action":"empty"}"#;
190        let action: Action = serde_json::from_str(json).unwrap();
191        assert_eq!(action, Action::Empty);
192    }
193
194    #[test]
195    fn test_deserialize_hash() {
196        let json = r#"{"action":"hash","length":null}"#;
197        let action: Action = serde_json::from_str(json).unwrap();
198        assert_eq!(action, Action::Hash { length: None });
199    }
200
201    #[test]
202    fn test_deserialize_hash_date() {
203        let json = r#"{"action":"hashdate"}"#;
204        let action: Action = serde_json::from_str(json).unwrap();
205        assert_eq!(action, Action::HashDate);
206    }
207
208    #[test]
209    fn test_deserialize_hash_uid() {
210        let json = r#"{"action":"hashuid"}"#;
211        let action: Action = serde_json::from_str(json).unwrap();
212        assert_eq!(action, Action::HashUID);
213    }
214
215    #[test]
216    fn test_deserialize_keep() {
217        let json = r#"{"action":"keep"}"#;
218        let action: Action = serde_json::from_str(json).unwrap();
219        assert_eq!(action, Action::Keep);
220    }
221
222    #[test]
223    fn test_deserialize_none() {
224        let json = r#"{"action":"none"}"#;
225        let action: Action = serde_json::from_str(json).unwrap();
226        assert_eq!(action, Action::None);
227    }
228
229    #[test]
230    fn test_deserialize_remove() {
231        let json = r#"{"action":"remove"}"#;
232        let action: Action = serde_json::from_str(json).unwrap();
233        assert_eq!(action, Action::Remove);
234    }
235
236    #[test]
237    fn test_deserialize_replace() {
238        let json = r#"{"action":"replace","value":"ANONYMIZED"}"#;
239        let action: Action = serde_json::from_str(json).unwrap();
240        assert_eq!(
241            action,
242            Action::Replace {
243                value: "ANONYMIZED".to_string()
244            }
245        );
246    }
247
248    #[test]
249    fn test_case_handling_on_deserialization() {
250        // This test passes - lowercase is expected
251        let json = r#"{"action":"empty"}"#;
252        let action: Action = serde_json::from_str(json).unwrap();
253        assert_eq!(action, Action::Empty);
254
255        // Uppercase will fail without aliases
256        let json = r#"{"action":"EMPTY"}"#;
257        let result: Result<Action, _> = serde_json::from_str(json);
258        assert!(result.is_err());
259
260        // Same for mixed case
261        let json = r#"{"action":"Hash"}"#;
262        let result: Result<Action, _> = serde_json::from_str(json);
263        assert!(result.is_err());
264    }
265
266    #[test]
267    fn test_roundtrip_all_variants() {
268        // Test all variants in one go
269        let variants = vec![
270            Action::Empty,
271            Action::Hash { length: None },
272            Action::HashDate,
273            Action::HashUID,
274            Action::Keep,
275            Action::None,
276            Action::Remove,
277            Action::Replace {
278                value: "TEST".to_string(),
279            },
280        ];
281
282        for variant in variants {
283            let json = serde_json::to_string(&variant).unwrap();
284            let deserialized: Action = serde_json::from_str(&json).unwrap();
285            assert_eq!(
286                variant, deserialized,
287                "Roundtrip failed for variant: {:?}",
288                variant
289            );
290        }
291    }
292
293    #[test]
294    fn test_error_handling_missing_action() {
295        let json = r#"{"with":"ANONYMIZED"}"#;
296        let result: Result<Action, _> = serde_json::from_str(json);
297        assert!(result.is_err());
298    }
299
300    #[test]
301    fn test_error_handling_invalid_action() {
302        let json = r#"{"action":"invalidaction"}"#;
303        let result: Result<Action, _> = serde_json::from_str(json);
304        assert!(result.is_err());
305    }
306
307    #[test]
308    fn test_error_handling_missing_replace_with() {
309        let json = r#"{"action":"replace"}"#;
310        let result: Result<Action, _> = serde_json::from_str(json);
311        assert!(result.is_err());
312    }
313
314    #[test]
315    fn test_pretty_print() {
316        let action = Action::Replace {
317            value: "ANONYMIZED".to_string(),
318        };
319        let json = serde_json::to_string_pretty(&action).unwrap();
320        let expected = r#"{
321  "action": "replace",
322  "value": "ANONYMIZED"
323}"#;
324        assert_eq!(json, expected);
325    }
326}