pub mod builder;
pub(crate) mod tag_action_map;
pub mod uid_root;
use crate::actions::Action;
use crate::hasher::{blake3_hash_fn, HashFn};
use crate::Tag;
use serde::{Deserialize, Serialize};
use tag_action_map::TagActionMap;
use thiserror::Error;
use uid_root::{UidRoot, UidRootError};
const DEIDENTIFIER: &str = "CARECODERS.IO";
#[derive(Error, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum ConfigError {
#[error("invalid UID root: {0}")]
InvalidUidRoot(String),
#[error("invalid hash length: {0}")]
InvalidHashLength(String),
}
impl From<UidRootError> for ConfigError {
fn from(err: UidRootError) -> Self {
ConfigError::InvalidUidRoot(err.0)
}
}
pub fn default_hash_fn() -> HashFn {
blake3_hash_fn
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Config {
#[serde(skip, default = "default_hash_fn")]
hash_fn: HashFn,
#[serde(default, skip_serializing_if = "Option::is_none")]
uid_root: Option<UidRoot>,
#[serde(default, skip_serializing_if = "Option::is_none")]
remove_private_tags: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
remove_curves: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
remove_overlays: Option<bool>,
#[serde(
default = "TagActionMap::default",
skip_serializing_if = "TagActionMap::is_empty"
)]
tag_actions: TagActionMap,
}
impl Config {
fn new(
hash_fn: HashFn,
uid_root: Option<UidRoot>,
remove_private_tags: Option<bool>,
remove_curves: Option<bool>,
remove_overlays: Option<bool>,
) -> Self {
Self {
hash_fn,
uid_root,
remove_private_tags,
remove_curves,
remove_overlays,
tag_actions: TagActionMap::new(),
}
}
}
impl Default for Config {
fn default() -> Self {
Self::new(blake3_hash_fn, None, None, None, None)
}
}
pub(crate) fn is_private_tag(tag: &Tag) -> bool {
tag.group() % 2 != 0
}
pub(crate) fn is_curve_tag(tag: &Tag) -> bool {
(tag.group() & 0xFF00) == 0x5000
}
pub(crate) fn is_overlay_tag(tag: &Tag) -> bool {
(tag.group() & 0xFF00) == 0x6000
}
impl Config {
pub fn get_hash_fn(&self) -> HashFn {
self.hash_fn
}
pub fn get_uid_root(&self) -> &Option<UidRoot> {
&self.uid_root
}
pub fn get_action(&self, tag: &Tag) -> &Action {
match self.tag_actions.get(tag) {
Some(action) if action == &Action::None && self.should_be_removed(tag) => {
&Action::Remove
}
Some(action) => action,
None if self.should_be_removed(tag) => &Action::Remove,
None => &Action::Keep,
}
}
fn should_be_removed(&self, tag: &Tag) -> bool {
(self.remove_private_tags.unwrap_or(false) && is_private_tag(tag))
|| (self.remove_curves.unwrap_or(false) && is_curve_tag(tag))
|| (self.remove_overlays.unwrap_or(false) && is_overlay_tag(tag))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tags;
use builder::ConfigBuilder;
use uid_root::UidRoot;
#[test]
fn test_config_builder() {
let config = ConfigBuilder::new()
.tag_action(tags::PATIENT_NAME, Action::Empty)
.build();
let tag_action = config.get_action(&tags::PATIENT_NAME);
assert_eq!(tag_action, &Action::Empty);
let tag_action = config.get_action(&tags::PATIENT_ID);
assert_eq!(tag_action, &Action::Keep);
}
#[test]
fn test_is_private_tag() {
assert!(is_private_tag(&Tag::from([1, 0])));
assert!(is_private_tag(&Tag::from([13, 12])));
assert!(is_private_tag(&Tag::from([33, 33])));
assert!(!is_private_tag(&tags::ACCESSION_NUMBER));
assert!(!is_private_tag(&tags::PATIENT_ID));
assert!(!is_private_tag(&tags::PIXEL_DATA));
}
#[test]
fn test_keep_private_tag() {
let tag = Tag(0x0033, 0x0010);
let config = ConfigBuilder::new()
.remove_private_tags(true)
.tag_action(tag, Action::Keep)
.build();
let tag_action = config.get_action(&tag);
assert_eq!(tag_action, &Action::Keep);
assert_eq!(config.get_action(&Tag(0x0033, 0x1010)), &Action::Remove);
assert_eq!(config.get_action(&tags::PATIENT_ID), &Action::Keep);
}
#[test]
fn test_remove_private_tag() {
let tag = Tag(0x0033, 0x0010);
let config = ConfigBuilder::new()
.remove_private_tags(true)
.tag_action(tag, Action::None)
.build();
let tag_action = config.get_action(&tag);
assert_eq!(tag_action, &Action::Remove);
assert_eq!(config.get_action(&Tag(0x0033, 0x1010)), &Action::Remove);
assert_eq!(config.get_action(&tags::PATIENT_ID), &Action::Keep);
}
#[test]
fn test_is_curve_tag() {
assert!(is_curve_tag(&Tag::from([0x5000, 0])));
assert!(is_curve_tag(&Tag::from([0x5010, 0x0011])));
assert!(is_curve_tag(&Tag::from([0x50FF, 0x0100])));
assert!(!is_curve_tag(&Tag::from([0x5100, 0])));
assert!(!is_curve_tag(&Tag::from([0x6000, 0])));
}
#[test]
fn test_keep_curve_tag() {
let tag = Tag(0x5010, 0x0011);
let config = ConfigBuilder::new()
.remove_curves(true)
.tag_action(tag, Action::Keep)
.build();
let tag_action = config.get_action(&tag);
assert_eq!(tag_action, &Action::Keep);
assert_eq!(config.get_action(&Tag(0x50FF, 0x0100)), &Action::Remove);
assert_eq!(config.get_action(&tags::PATIENT_ID), &Action::Keep);
}
#[test]
fn test_remove_curve_tag() {
let tag = Tag(0x5010, 0x0011);
let config = ConfigBuilder::new()
.remove_curves(true)
.tag_action(tag, Action::None)
.build();
let tag_action = config.get_action(&tag);
assert_eq!(tag_action, &Action::Remove);
assert_eq!(config.get_action(&Tag(0x50FF, 0x0100)), &Action::Remove);
assert_eq!(config.get_action(&tags::PATIENT_ID), &Action::Keep);
}
#[test]
fn test_is_overlay_tag() {
assert!(is_overlay_tag(&Tag::from([0x6000, 0])));
assert!(is_overlay_tag(&Tag::from([0x6010, 0x0011])));
assert!(is_overlay_tag(&Tag::from([0x60FF, 0x0100])));
assert!(!is_overlay_tag(&Tag::from([0x6100, 0])));
assert!(!is_overlay_tag(&Tag::from([0x5000, 0])));
}
#[test]
fn test_keep_overlay_tag() {
let tag = Tag(0x6010, 0x0011);
let config = ConfigBuilder::new()
.remove_overlays(true)
.tag_action(tag, Action::Keep)
.build();
let tag_action = config.get_action(&tag);
assert_eq!(tag_action, &Action::Keep);
assert_eq!(config.get_action(&Tag(0x60FF, 0x0100)), &Action::Remove);
assert_eq!(config.get_action(&tags::PATIENT_ID), &Action::Keep);
}
#[test]
fn test_remove_overlay_tag() {
let tag = Tag(0x6010, 0x0011);
let config = ConfigBuilder::new()
.remove_overlays(true)
.tag_action(tag, Action::None)
.build();
let tag_action = config.get_action(&tag);
assert_eq!(tag_action, &Action::Remove);
assert_eq!(config.get_action(&Tag(0x60FF, 0x0100)), &Action::Remove);
assert_eq!(config.get_action(&tags::PATIENT_ID), &Action::Keep);
}
fn create_sample_tag_actions() -> TagActionMap {
let mut map = TagActionMap::new(); map.insert(Tag(0x0010, 0x0010), Action::Empty); map.insert(Tag(0x0010, 0x0020), Action::Remove); map.insert(Tag(0x0008, 0x0050), Action::Hash { length: None }); map
}
#[test]
fn test_config_serialization() {
let config = Config {
uid_root: Some(UidRoot("1.2.826.0.1.3680043.10.188".to_string())),
tag_actions: create_sample_tag_actions(),
remove_private_tags: Some(true),
remove_curves: Some(false),
remove_overlays: Some(true),
..Default::default()
};
let json = serde_json::to_string_pretty(&config).unwrap();
assert!(json.contains(r#""uid_root": "1.2.826.0.1.3680043.10.188"#));
assert!(json.contains(r#""remove_private_tags": true"#));
assert!(json.contains(r#""remove_curves": false"#));
assert!(json.contains(r#""remove_overlays": true"#));
assert!(json.contains(r#""00100010""#)); assert!(json.contains(r#""action": "empty""#));
assert!(json.contains(r#""00100020""#)); assert!(json.contains(r#""action": "remove""#));
assert!(json.contains(r#""00080050""#)); assert!(json.contains(r#""action": "hash""#));
}
#[test]
fn test_config_deserialization() {
let json = r#"{
"uid_root": "1.2.826.0.1.3680043.10.188",
"remove_private_tags": true,
"remove_curves": false,
"remove_overlays": true,
"tag_actions": {
"(0010,0010)": {"action": "empty"},
"(0010,0020)": {"action": "remove"},
"(0008,0050)": {"action": "hash"}
}
}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.uid_root.unwrap().0, "1.2.826.0.1.3680043.10.188");
assert_eq!(config.remove_private_tags, Some(true));
assert_eq!(config.remove_curves, Some(false));
assert_eq!(config.remove_overlays, Some(true));
let patient_name = config.tag_actions.get(&Tag(0x0010, 0x0010)).unwrap();
match patient_name {
Action::Empty => { }
_ => panic!("Expected Empty action for Patient Name"),
}
let patient_id = config.tag_actions.get(&Tag(0x0010, 0x0020)).unwrap();
match patient_id {
Action::Remove => { }
_ => panic!("Expected Remove action for Patient ID"),
}
let accession = config.tag_actions.get(&Tag(0x0008, 0x0050)).unwrap();
match accession {
Action::Hash { length } => {
assert_eq!(*length, None);
}
_ => panic!("Expected Hash action for Accession Number"),
}
}
#[test]
fn test_config_roundtrip() {
let original_config = Config {
uid_root: Some(UidRoot("1.2.826.0.1.3680043.10.188".to_string())),
tag_actions: create_sample_tag_actions(),
remove_private_tags: Some(true),
remove_curves: Some(false),
remove_overlays: Some(true),
..Default::default()
};
let json = serde_json::to_string(&original_config).unwrap();
let deserialized: Config = serde_json::from_str(&json).unwrap();
assert_eq!(
original_config.uid_root.unwrap().0,
deserialized.uid_root.unwrap().0
);
assert_eq!(
original_config.remove_private_tags,
deserialized.remove_private_tags
);
assert_eq!(original_config.remove_curves, deserialized.remove_curves);
assert_eq!(
original_config.remove_overlays,
deserialized.remove_overlays
);
let tags_to_check = [
Tag(0x0010, 0x0010), Tag(0x0010, 0x0020), Tag(0x0008, 0x0050), ];
for tag in &tags_to_check {
let original_action = original_config.tag_actions.get(tag);
let deserialized_action = deserialized.tag_actions.get(tag);
assert_eq!(
original_action, deserialized_action,
"Action for tag ({}) didn't roundtrip correctly",
tag,
);
}
}
#[test]
fn test_empty_tag_actions() {
let empty_map = TagActionMap::new();
let config = Config {
uid_root: Some(UidRoot("1.2.826.0.1.3680043.10.188".to_string())),
tag_actions: empty_map,
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: Config = serde_json::from_str(&json).unwrap();
assert_eq!(
deserialized.uid_root.unwrap().0,
"1.2.826.0.1.3680043.10.188"
);
assert_eq!(deserialized.remove_private_tags, None);
assert_eq!(deserialized.remove_curves, None);
assert_eq!(deserialized.remove_overlays, None);
assert_eq!(deserialized.tag_actions.len(), 0);
}
#[test]
fn test_partial_config_deserialization() {
let json = r#"{
"uid_root": "1.2.826.0.1.3680043.10.188",
"tag_actions": {
"(0010,0010)": {"action": "empty"}
}
}"#;
let result: Result<Config, _> = serde_json::from_str(json);
let config = result.unwrap();
assert_eq!(config.uid_root.unwrap().0, "1.2.826.0.1.3680043.10.188");
assert_eq!(config.remove_private_tags, None);
assert_eq!(config.remove_curves, None);
assert_eq!(config.remove_overlays, None);
assert_eq!(config.tag_actions.len(), 1);
}
#[test]
fn test_empty_uid_root_and_tag_actions() {
let json = r#"{
"uid_root": "",
"remove_private_tags": true,
"remove_curves": false,
"remove_overlays": true,
"tag_actions": {}
}"#;
let result: Result<Config, _> = serde_json::from_str(json);
let config = result.unwrap();
assert_eq!(config.uid_root.unwrap().0, "");
assert_eq!(config.remove_private_tags, Some(true));
assert_eq!(config.remove_curves, Some(false));
assert_eq!(config.remove_overlays, Some(true));
assert_eq!(config.tag_actions.len(), 0);
}
#[test]
fn test_missing_uid_root() {
let json = r#"{
"remove_private_tags": true,
"remove_curves": false,
"remove_overlays": true,
"tag_actions": {}
}"#;
let result: Result<Config, _> = serde_json::from_str(json);
let config = result.unwrap();
assert_eq!(config.uid_root, None);
assert_eq!(config.remove_private_tags, Some(true));
assert_eq!(config.remove_curves, Some(false));
assert_eq!(config.remove_overlays, Some(true));
assert_eq!(config.tag_actions.len(), 0);
}
#[test]
fn test_default_remove_fields() {
let json = r#"{
"uid_root": "9999",
"tag_actions": {}
}"#;
let result: Result<Config, _> = serde_json::from_str(json);
let config = result.unwrap();
assert_eq!(config.uid_root, Some(UidRoot("9999".into())));
assert_eq!(config.remove_private_tags, None);
assert_eq!(config.remove_curves, None);
assert_eq!(config.remove_overlays, None);
assert_eq!(config.tag_actions.len(), 0);
}
#[test]
fn test_only_empty_tag_actions() {
let json = r#"{
"tag_actions": {}
}"#;
let result: Result<Config, _> = serde_json::from_str(json);
let config = result.unwrap();
assert_eq!(config.uid_root, None);
assert_eq!(config.remove_private_tags, None);
assert_eq!(config.remove_curves, None);
assert_eq!(config.remove_overlays, None);
assert_eq!(config.tag_actions.len(), 0);
}
#[test]
fn test_empty_json() {
let json = r#"{}"#;
let result: Result<Config, _> = serde_json::from_str(json);
let config = result.unwrap();
assert_eq!(config.uid_root, None);
assert_eq!(config.remove_private_tags, None);
assert_eq!(config.remove_curves, None);
assert_eq!(config.remove_overlays, None);
assert_eq!(config.tag_actions.len(), 0);
}
#[test]
fn test_malformed_config() {
let json = r#"{
"uid_root": "1.2.826.0.1.3680043.10.188",
"remove_private_tags": true,
"remove_curves": false,
"remove_overlays": true,
"tag_actions": {
"invalid_tag_format": {"action": "empty"}
}
}"#;
let result: Result<Config, _> = serde_json::from_str(json);
assert!(result.is_err());
let json = r#"{
"uid_root": "1.2.826.0.1.3680043.10.188",
"remove_private_tags": true,
"remove_curves": false,
"remove_overlays": true,
"tag_actions": {
"(0010,0010)": {"action": "invalid_action"}
},
}"#;
let result: Result<Config, _> = serde_json::from_str(json);
assert!(result.is_err());
}
}