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        let response = request
76            .send()
77            .await
78            .context(RequestFailedSnafu { url: &self.url })?;
79
80        if !response.status().is_success() {
81            return Err(DicomWebError::HttpStatusFailure {
82                status_code: response.status(),
83            });
84        }
85
86        // Check if the response is a DICOM-JSON
87        let ct = response
88            .headers()
89            .get("Content-Type")
90            .ok_or(DicomWebError::MissingContentTypeHeader)?;
91        let media_type = MediaType::parse(ct.to_str().unwrap_or_default())
92            .map_err(|e| DicomWebError::ContentTypeParseFailed { source: e })?;
93
94        // Check if we have a DICOM-JSON or JSON content type
95        if media_type.essence() != MediaType::new(APPLICATION, JSON)
96            && media_type.essence()
97                != MediaType::from_parts(APPLICATION, Name::new_unchecked("dicom"), Some(JSON), &[])
98        {
99            return Err(DicomWebError::UnexpectedContentType {
100                content_type: ct.to_str().unwrap_or_default().to_string(),
101            });
102        }
103
104        Ok(response
105            .json::<Vec<DicomJson<InMemDicomObject>>>()
106            .await
107            .context(DeserializationFailedSnafu {})?
108            .into_iter()
109            .map(|dj| dj.into_inner())
110            .collect())
111    }
112
113    /// Set the maximum number of results to return. Will be passed as a query parameter.
114    /// This is useful for pagination.
115    pub fn with_limit(&mut self, limit: u32) -> &mut Self {
116        self.limit = Some(limit);
117        self
118    }
119
120    /// Set the offset of the results to return. Will be passed as a query parameter.
121    /// This is useful for pagination.
122    pub fn with_offset(&mut self, offset: u32) -> &mut Self {
123        self.offset = Some(offset);
124        self
125    }
126
127    /// Set the tags that should be queried. Will be passed as a query parameter.
128    pub fn with_includefields(&mut self, includefields: Vec<Tag>) -> &mut Self {
129        self.includefields = includefields;
130        self
131    }
132
133    /// Set whether fuzzy matching should be used. Will be passed as a query parameter.
134    pub fn with_fuzzymatching(&mut self, fuzzymatching: bool) -> &mut Self {
135        self.fuzzymatching = Some(fuzzymatching);
136        self
137    }
138
139    /// Add a filter to the query. Will be passed as a query parameter.
140    pub fn with_filter(&mut self, tag: Tag, value: String) -> &mut Self {
141        self.filters.push((tag, value));
142        self
143    }
144}
145
146impl DicomWebClient {
147    /// Create a QIDO-RS request to query all studies
148    pub fn query_studies(&self) -> QidoRequest {
149        let base_url = &self.qido_url;
150        let url = format!("{base_url}/studies");
151
152        QidoRequest::new(self.clone(), url)
153    }
154
155    /// Create a QIDO-RS request to query all series
156    pub fn query_series(&self) -> QidoRequest {
157        let base_url = &self.qido_url;
158        let url = format!("{base_url}/series");
159
160        QidoRequest::new(self.clone(), url)
161    }
162
163    /// Create a QIDO-RS request to query all series in a specific study
164    pub fn query_series_in_study(&self, study_instance_uid: &str) -> QidoRequest {
165        let base_url = &self.qido_url;
166        let url = format!("{base_url}/studies/{study_instance_uid}/series");
167
168        QidoRequest::new(self.clone(), url)
169    }
170
171    /// Create a QIDO-RS request to query all instances
172    pub fn query_instances(&self) -> QidoRequest {
173        let base_url = &self.qido_url;
174        let url = format!("{base_url}/instances");
175
176        QidoRequest::new(self.clone(), url)
177    }
178
179    /// Create a QIDO-RS request to query all instances in a specific series
180    pub fn query_instances_in_series(
181        &self,
182        study_instance_uid: &str,
183        series_instance_uid: &str,
184    ) -> QidoRequest {
185        let base_url = &self.qido_url;
186        let url = format!(
187            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances",
188        );
189
190        QidoRequest::new(self.clone(), url)
191    }
192}