dicom_web/
qido.rs

1//! Module for QIDO-RS requests
2use dicom_core::Tag;
3use dicom_json::DicomJson;
4use dicom_object::InMemDicomObject;
5
6use mediatype::{
7    names::{APPLICATION, JSON},
8    MediaType, Name,
9};
10use snafu::ResultExt;
11
12use crate::{DeserializationFailedSnafu, DicomWebClient, DicomWebError, RequestFailedSnafu};
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<(Tag, 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        for include_field in self.includefields.iter() {
51            // Convert the tag to a radix string
52            let radix_string = format!(
53                "{:04x}{:04x}",
54                include_field.group(),
55                include_field.element()
56            );
57
58            query.push((String::from("includefield"), radix_string));
59        }
60        for filter in self.filters.iter() {
61            query.push((filter.0.to_string(), filter.1.clone()));
62        }
63
64        let mut request = self.client.client.get(&self.url).query(&query);
65
66        // Basic authentication
67        if let Some(username) = &self.client.username {
68            request = request.basic_auth(username, self.client.password.as_ref());
69        }
70        // Bearer token
71        else if let Some(bearer_token) = &self.client.bearer_token {
72            request = request.bearer_auth(bearer_token);
73        }
74
75        // Extra headers
76        for (key, value) in &self.client.extra_headers {
77            request = request.header(key, value);
78        }
79
80        let response = request
81            .send()
82            .await
83            .context(RequestFailedSnafu { url: &self.url })?;
84
85        if !response.status().is_success() {
86            return Err(DicomWebError::HttpStatusFailure {
87                status_code: response.status(),
88            });
89        }
90
91        // Check if the response is a DICOM-JSON
92        let ct = response
93            .headers()
94            .get("Content-Type")
95            .ok_or(DicomWebError::MissingContentTypeHeader)?;
96        let media_type = MediaType::parse(ct.to_str().unwrap_or_default())
97            .map_err(|e| DicomWebError::ContentTypeParseFailed { source: e })?;
98
99        // Check if we have a DICOM-JSON or JSON content type
100        if media_type.essence() != MediaType::new(APPLICATION, JSON)
101            && media_type.essence()
102                != MediaType::from_parts(APPLICATION, Name::new_unchecked("dicom"), Some(JSON), &[])
103        {
104            return Err(DicomWebError::UnexpectedContentType {
105                content_type: ct.to_str().unwrap_or_default().to_string(),
106            });
107        }
108
109        Ok(response
110            .json::<Vec<DicomJson<InMemDicomObject>>>()
111            .await
112            .context(DeserializationFailedSnafu {})?
113            .into_iter()
114            .map(|dj| dj.into_inner())
115            .collect())
116    }
117
118    /// Set the maximum number of results to return. Will be passed as a query parameter.
119    /// This is useful for pagination.
120    pub fn with_limit(&mut self, limit: u32) -> &mut Self {
121        self.limit = Some(limit);
122        self
123    }
124
125    /// Set the offset of the results to return. Will be passed as a query parameter.
126    /// This is useful for pagination.
127    pub fn with_offset(&mut self, offset: u32) -> &mut Self {
128        self.offset = Some(offset);
129        self
130    }
131
132    /// Set the tags that should be queried. Will be passed as a query parameter.
133    pub fn with_includefields(&mut self, includefields: Vec<Tag>) -> &mut Self {
134        self.includefields = includefields;
135        self
136    }
137
138    /// Set whether fuzzy matching should be used. Will be passed as a query parameter.
139    pub fn with_fuzzymatching(&mut self, fuzzymatching: bool) -> &mut Self {
140        self.fuzzymatching = Some(fuzzymatching);
141        self
142    }
143
144    /// Add a filter to the query. Will be passed as a query parameter.
145    pub fn with_filter(&mut self, tag: Tag, value: String) -> &mut Self {
146        self.filters.push((tag, value));
147        self
148    }
149}
150
151impl DicomWebClient {
152    /// Create a QIDO-RS request to query all studies
153    pub fn query_studies(&self) -> QidoRequest {
154        let base_url = &self.qido_url;
155        let url = format!("{base_url}/studies");
156
157        QidoRequest::new(self.clone(), url)
158    }
159
160    /// Create a QIDO-RS request to query all series
161    pub fn query_series(&self) -> QidoRequest {
162        let base_url = &self.qido_url;
163        let url = format!("{base_url}/series");
164
165        QidoRequest::new(self.clone(), url)
166    }
167
168    /// Create a QIDO-RS request to query all series in a specific study
169    pub fn query_series_in_study(&self, study_instance_uid: &str) -> QidoRequest {
170        let base_url = &self.qido_url;
171        let url = format!("{base_url}/studies/{study_instance_uid}/series");
172
173        QidoRequest::new(self.clone(), url)
174    }
175
176    /// Create a QIDO-RS request to query all instances
177    pub fn query_instances(&self) -> QidoRequest {
178        let base_url = &self.qido_url;
179        let url = format!("{base_url}/instances");
180
181        QidoRequest::new(self.clone(), url)
182    }
183
184    /// Create a QIDO-RS request to query all instances in a specific series
185    pub fn query_instances_in_series(
186        &self,
187        study_instance_uid: &str,
188        series_instance_uid: &str,
189    ) -> QidoRequest {
190        let base_url = &self.qido_url;
191        let url = format!(
192            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances",
193        );
194
195        QidoRequest::new(self.clone(), url)
196    }
197}