Skip to content

Commit 7c1750b

Browse files
committed
feat(qido-rs,mwl-rs): support sequence attribute filtering
1 parent 93e6426 commit 7c1750b

5 files changed

Lines changed: 67 additions & 37 deletions

File tree

src/api/mod.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use std::fmt::Formatter;
33

44
use crate::AppState;
55
use axum::Router;
6-
use dicom::core::dictionary::{DataDictionaryEntry, DataDictionaryEntryRef};
6+
use dicom::core::dictionary::DataDictionaryEntry;
7+
use dicom::core::ops::AttributeSelector;
78
use dicom::core::{DataDictionary, PrimitiveValue, Tag, VR};
89
use dicom::object::StandardDataDictionary;
910
use serde::de::{Error, SeqAccess, Visitor};
@@ -39,10 +40,10 @@ pub fn routes(base_path: &str) -> Router<AppState> {
3940
/// Match Query Parameters for QIDO and MWL requests.
4041
#[derive(Debug, Deserialize, PartialEq)]
4142
#[serde(try_from = "HashMap<String, String>")]
42-
pub struct MatchCriteria(Vec<(Tag, PrimitiveValue)>);
43+
pub struct MatchCriteria(Vec<(AttributeSelector, PrimitiveValue)>);
4344

4445
impl MatchCriteria {
45-
pub fn into_inner(self) -> Vec<(Tag, PrimitiveValue)> {
46+
pub fn into_inner(self) -> Vec<(AttributeSelector, PrimitiveValue)> {
4647
self.0
4748
}
4849
}
@@ -51,15 +52,15 @@ impl TryFrom<HashMap<String, String>> for MatchCriteria {
5152
type Error = String;
5253

5354
fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
54-
let criteria: Vec<(Tag, PrimitiveValue)> = value
55+
let criteria: Vec<(AttributeSelector, PrimitiveValue)> = value
5556
.into_iter()
5657
.map(|(key, value)| {
5758
StandardDataDictionary
58-
.by_expr(&key)
59-
.ok_or(format!("Cannot use unknown attribute {key} for matching."))
60-
.and_then(|entry| {
61-
to_primitive_value(entry, &value)
62-
.map(|primitive| (entry.tag.inner(), primitive))
59+
.parse_selector(&key)
60+
.map_err(|err| format!("invalid attribute selector {key}: {err}"))
61+
.and_then(|selector| {
62+
to_primitive_value(selector.last_tag(), &value)
63+
.map(|primitive| (selector, primitive))
6364
})
6465
})
6566
.collect::<Result<_, Self::Error>>()?;
@@ -68,14 +69,15 @@ impl TryFrom<HashMap<String, String>> for MatchCriteria {
6869
}
6970

7071
/// helper function to convert a query parameter value to a PrimitiveValue
71-
fn to_primitive_value(
72-
entry: &DataDictionaryEntryRef,
73-
raw_value: &str,
74-
) -> Result<PrimitiveValue, String> {
72+
fn to_primitive_value(tag: Tag, raw_value: &str) -> Result<PrimitiveValue, String> {
7573
if raw_value.is_empty() {
7674
return Ok(PrimitiveValue::Empty);
7775
}
78-
match entry.vr.relaxed() {
76+
let vr = StandardDataDictionary
77+
.by_tag(tag)
78+
.ok_or_else(|| format!("unknown tag {tag}"))?
79+
.vr();
80+
match vr.relaxed() {
7981
// String-like VRs, no parsing required
8082
VR::AE
8183
| VR::AS
@@ -133,9 +135,8 @@ fn to_primitive_value(
133135
Ok(PrimitiveValue::from(value))
134136
}
135137
_ => Err(format!(
136-
"Attribute {} cannot be used for matching due to unsupported VR {}",
137-
entry.tag(),
138-
entry.vr.relaxed()
138+
"Attribute {} cannot be used for matching due to unsupported VR {:?}",
139+
tag, vr
139140
)),
140141
}
141142
}

src/api/mwl/service.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub enum MwlSearchError {
8181
mod tests {
8282
use axum::extract::Query;
8383
use axum::http::Uri;
84+
use dicom::core::ops::AttributeSelector;
8485
use dicom::core::PrimitiveValue;
8586
use dicom::dictionary_std::tags;
8687

@@ -100,14 +101,41 @@ mod tests {
100101
limit: 42,
101102
include_field: IncludeField::List(vec![tags::PATIENT_WEIGHT]),
102103
match_criteria: MatchCriteria(vec![(
103-
tags::PATIENT_NAME,
104+
AttributeSelector::from(tags::PATIENT_NAME),
104105
PrimitiveValue::from("MUSTERMANN^MAX")
105106
)]),
106107
fuzzy_matching: false,
107108
}
108109
);
109110
}
110111

112+
#[test]
113+
fn parse_query_params_nested() {
114+
let uri = Uri::from_static(
115+
"http://test?00400100.00400010=CTSCANNER",
116+
);
117+
let Query(params) = Query::<MwlQueryParameters>::try_from_uri(&uri).unwrap();
118+
119+
assert_eq!(
120+
params,
121+
MwlQueryParameters {
122+
offset: 0,
123+
limit: 200,
124+
include_field: IncludeField::List(vec![]),
125+
match_criteria: MatchCriteria(vec![
126+
(
127+
AttributeSelector::from((
128+
tags::SCHEDULED_PROCEDURE_STEP_SEQUENCE,
129+
tags::SCHEDULED_STATION_NAME
130+
)),
131+
PrimitiveValue::from("CTSCANNER")
132+
)
133+
]),
134+
fuzzy_matching: false,
135+
}
136+
);
137+
}
138+
111139
#[test]
112140
fn parse_query_params_multiple_includefield() {
113141
let uri =

src/api/qido/service.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ mod tests {
109109
use axum::extract::Query;
110110
use axum::http::Uri;
111111
use dicom::core::PrimitiveValue;
112-
use dicom::dictionary_std::tags;
112+
use dicom::core::ops::AttributeSelector;
113+
use dicom::dictionary_std::tags;
113114

114115
use super::*;
115116

@@ -127,7 +128,7 @@ mod tests {
127128
limit: 42,
128129
include_field: IncludeField::List(vec![tags::PATIENT_WEIGHT]),
129130
match_criteria: MatchCriteria(vec![(
130-
tags::PATIENT_NAME,
131+
AttributeSelector::from(tags::PATIENT_NAME),
131132
PrimitiveValue::from("MUSTERMANN^MAX")
132133
)]),
133134
fuzzy_matching: false,
@@ -165,7 +166,7 @@ mod tests {
165166
limit: 200,
166167
include_field: IncludeField::List(Vec::new()),
167168
match_criteria: MatchCriteria(vec![(
168-
tags::STUDY_INSTANCE_UID,
169+
AttributeSelector::from(tags::STUDY_INSTANCE_UID),
169170
PrimitiveValue::Strs(
170171
vec![String::from("1"), String::from("2"), String::from("3")].into()
171172
)
@@ -187,7 +188,7 @@ mod tests {
187188
limit: 200,
188189
include_field: IncludeField::List(Vec::new()),
189190
match_criteria: MatchCriteria(vec![(
190-
tags::STUDY_INSTANCE_UID,
191+
AttributeSelector::from(tags::STUDY_INSTANCE_UID),
191192
PrimitiveValue::from("1.2.3")
192193
)]),
193194
fuzzy_matching: false,

src/backend/dimse/mwl.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ impl MwlService for DimseMwlService {
3737
let default_tags = WORKITEM_SEARCH_TAGS;
3838

3939
for tag in default_tags {
40-
attributes.push((*tag, PrimitiveValue::Empty));
40+
attributes.push((AttributeSelector::from(*tag), PrimitiveValue::Empty));
4141
}
4242

43-
for (tag, value) in request.parameters.match_criteria.into_inner() {
44-
attributes.push((tag, value));
43+
for (selector, value) in request.parameters.match_criteria.into_inner() {
44+
attributes.push((selector, value));
4545
}
4646

4747
match request.parameters.include_field {
@@ -52,13 +52,13 @@ impl MwlService for DimseMwlService {
5252
}
5353
IncludeField::List(tags) => {
5454
for tag in tags {
55-
attributes.push((tag, PrimitiveValue::Empty));
55+
attributes.push((AttributeSelector::from(tag), PrimitiveValue::Empty));
5656
}
5757
}
5858
};
59-
for (tag, value) in attributes {
59+
for (selector, value) in attributes {
6060
if let Err(err) = identifier.apply(AttributeOp::new(
61-
AttributeSelector::from(tag),
61+
selector,
6262
AttributeAction::Set(value),
6363
)) {
6464
warn!("Skipped attribute operation: {err}");

src/backend/dimse/qido.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ impl QidoService for DimseQidoService {
4545
};
4646

4747
for tag in default_tags {
48-
attributes.push((*tag, PrimitiveValue::Empty));
48+
attributes.push((AttributeSelector::from(*tag), PrimitiveValue::Empty));
4949
}
5050

51-
for (tag, value) in request.parameters.match_criteria.into_inner() {
52-
attributes.push((tag, value));
51+
for (selector, value) in request.parameters.match_criteria.into_inner() {
52+
attributes.push((selector, value));
5353
}
5454

5555
match request.parameters.include_field {
@@ -60,27 +60,27 @@ impl QidoService for DimseQidoService {
6060
}
6161
IncludeField::List(tags) => {
6262
for tag in tags {
63-
attributes.push((tag, PrimitiveValue::Empty));
63+
attributes.push((AttributeSelector::from(tag), PrimitiveValue::Empty));
6464
}
6565
}
6666
};
6767

6868
attributes.push((
69-
tags::QUERY_RETRIEVE_LEVEL,
69+
AttributeSelector::from(tags::QUERY_RETRIEVE_LEVEL),
7070
PrimitiveValue::from(request.query.query_retrieve_level),
7171
));
7272

7373
if let Some(study) = request.query.study_instance_uid {
74-
attributes.push((tags::STUDY_INSTANCE_UID, PrimitiveValue::from(study)));
74+
attributes.push((AttributeSelector::from(tags::STUDY_INSTANCE_UID), PrimitiveValue::from(study)));
7575
}
7676

7777
if let Some(series) = request.query.series_instance_uid {
78-
attributes.push((tags::SERIES_INSTANCE_UID, PrimitiveValue::from(series)));
78+
attributes.push((AttributeSelector::from(tags::SERIES_INSTANCE_UID), PrimitiveValue::from(series)));
7979
}
8080

81-
for (tag, value) in attributes {
81+
for (selector, value) in attributes {
8282
if let Err(err) = identifier.apply(AttributeOp::new(
83-
AttributeSelector::from(tag),
83+
selector,
8484
AttributeAction::Set(value),
8585
)) {
8686
warn!("Skipped attribute operation: {err}");

0 commit comments

Comments
 (0)