Skip to main content

dicom_web/
qido.rs

1//! Module for QIDO-RS requests
2//! See https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6
3use dicom_core::{ops::AttributeSelector, Tag};
4use dicom_json::DicomJson;
5use dicom_object::InMemDicomObject;
6
7use snafu::ResultExt;
8
9use crate::{
10    apply_auth_and_headers, selector_to_string, validate_dicom_json_content_type,
11    DeserializationFailedSnafu, DicomWebClient, DicomWebError, RequestFailedSnafu,
12};
13
14/// A builder type for QIDO-RS requests
15/// By default, the request is built with no filters, no limit, and no offset.
16#[derive(Debug, Clone)]
17pub struct QidoRequest {
18    client: DicomWebClient,
19    url: String,
20
21    limit: Option<u32>,
22    offset: Option<u32>,
23    includefields: Vec<Tag>,
24    fuzzymatching: Option<bool>,
25    filters: Vec<(AttributeSelector, String)>,
26}
27
28impl QidoRequest {
29    fn new(client: DicomWebClient, url: String) -> Self {
30        QidoRequest {
31            client,
32            url,
33            limit: None,
34            offset: None,
35            includefields: vec![],
36            fuzzymatching: None,
37            filters: vec![],
38        }
39    }
40
41    /// Execute the QIDO-RS request
42    pub async fn run(self) -> Result<Vec<InMemDicomObject>, DicomWebError> {
43        let mut query: Vec<(String, String)> = vec![];
44        if let Some(limit) = self.limit {
45            query.push((String::from("limit"), limit.to_string()));
46        }
47        if let Some(offset) = self.offset {
48            query.push((String::from("offset"), offset.to_string()));
49        }
50        if let Some(fuzzymatching) = self.fuzzymatching {
51            query.push((String::from("fuzzymatching"), fuzzymatching.to_string()));
52        }
53        for include_field in self.includefields.iter() {
54            // Convert the tag to a radix string
55            let radix_string = format!(
56                "{:04x}{:04x}",
57                include_field.group(),
58                include_field.element()
59            );
60
61            query.push((String::from("includefield"), radix_string));
62        }
63        for (selector, value) in self.filters.iter() {
64            query.push((selector_to_string(&selector), value.clone()));
65        }
66
67        let mut request = self.client.client.get(&self.url).query(&query);
68        request = apply_auth_and_headers(request, &self.client);
69
70        let response = request
71            .send()
72            .await
73            .context(RequestFailedSnafu { url: &self.url })?;
74
75        if !response.status().is_success() {
76            return Err(DicomWebError::HttpStatusFailure {
77                status_code: response.status(),
78            });
79        }
80
81        // Check if the response is a DICOM-JSON
82        let ct = response
83            .headers()
84            .get("Content-Type")
85            .ok_or(DicomWebError::MissingContentTypeHeader)?;
86        validate_dicom_json_content_type(ct.to_str().unwrap_or_default())?;
87
88        Ok(response
89            .json::<Vec<DicomJson<InMemDicomObject>>>()
90            .await
91            .context(DeserializationFailedSnafu {})?
92            .into_iter()
93            .map(|dicomjson| dicomjson.into_inner())
94            .collect())
95    }
96
97    /// Set the maximum number of results to return. Will be passed as a query parameter.
98    /// This is useful for pagination.
99    pub fn with_limit(mut self, limit: u32) -> Self {
100        self.limit = Some(limit);
101        self
102    }
103
104    /// Set the offset of the results to return. Will be passed as a query parameter.
105    /// This is useful for pagination.
106    pub fn with_offset(mut self, offset: u32) -> Self {
107        self.offset = Some(offset);
108        self
109    }
110
111    /// Set the tags that should be queried. Will be passed as a query parameter.
112    pub fn with_includefields(mut self, includefields: Vec<Tag>) -> Self {
113        self.includefields = includefields;
114        self
115    }
116
117    /// Set whether fuzzy matching should be used. Will be passed as a query parameter.
118    pub fn with_fuzzymatching(mut self, fuzzymatching: bool) -> Self {
119        self.fuzzymatching = Some(fuzzymatching);
120        self
121    }
122
123    /// Add a filter to the query. Will be passed as a query parameter.
124    pub fn with_filter(mut self, selector: AttributeSelector, value: String) -> Self {
125        self.filters.push((selector, value));
126        self
127    }
128}
129
130impl DicomWebClient {
131    /// Create a QIDO-RS request to query all studies
132    pub fn query_studies(&self) -> QidoRequest {
133        let base_url = &self.qido_url;
134        let url = format!("{base_url}/studies");
135
136        QidoRequest::new(self.clone(), url)
137    }
138
139    /// Create a QIDO-RS request to query all series
140    pub fn query_series(&self) -> QidoRequest {
141        let base_url = &self.qido_url;
142        let url = format!("{base_url}/series");
143
144        QidoRequest::new(self.clone(), url)
145    }
146
147    /// Create a QIDO-RS request to query all series in a specific study
148    pub fn query_series_in_study(&self, study_instance_uid: &str) -> QidoRequest {
149        let base_url = &self.qido_url;
150        let url = format!("{base_url}/studies/{study_instance_uid}/series");
151
152        QidoRequest::new(self.clone(), url)
153    }
154
155    /// Create a QIDO-RS request to query all instances
156    pub fn query_instances(&self) -> QidoRequest {
157        let base_url = &self.qido_url;
158        let url = format!("{base_url}/instances");
159
160        QidoRequest::new(self.clone(), url)
161    }
162
163    /// Create a QIDO-RS request to query all instances in a specific series
164    pub fn query_instances_in_series(
165        &self,
166        study_instance_uid: &str,
167        series_instance_uid: &str,
168    ) -> QidoRequest {
169        let base_url = &self.qido_url;
170        let url = format!(
171            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances",
172        );
173
174        QidoRequest::new(self.clone(), url)
175    }
176}