dicom_web/
wado.rs

1//! Module for WADO-RS requests
2use dicom_json::DicomJson;
3use dicom_object::{from_reader, FileDicomObject, InMemDicomObject};
4
5use futures_util::{Stream, StreamExt};
6use mediatype::{
7    names::{APPLICATION, JSON},
8    MediaType, Name,
9};
10use multipart_rs::{MultipartItem, MultipartReader, MultipartType};
11use snafu::{OptionExt, ResultExt};
12
13use crate::{
14    DeserializationFailedSnafu, DicomReaderFailedSnafu, DicomWebClient, DicomWebError,
15    EmptyResponseSnafu, MissingContentTypeHeaderSnafu, MultipartReaderFailedSnafu,
16    RequestFailedSnafu,
17};
18
19/// A builder type for WADO-RS metadata requests
20#[derive(Debug, Clone)]
21pub struct WadoMetadataRequest {
22    client: DicomWebClient,
23    url: String,
24}
25
26impl WadoMetadataRequest {
27    fn new(client: DicomWebClient, url: String) -> Self {
28        WadoMetadataRequest { client, url }
29    }
30
31    pub async fn run(&self) -> Result<Vec<InMemDicomObject>, DicomWebError> {
32        let mut request = self.client.client.get(&self.url);
33
34        // Basic authentication
35        if let Some(username) = &self.client.username {
36            request = request.basic_auth(username, self.client.password.as_ref());
37        }
38        // Bearer token
39        else if let Some(bearer_token) = &self.client.bearer_token {
40            request = request.bearer_auth(bearer_token);
41        }
42
43        let response = request
44            .send()
45            .await
46            .context(RequestFailedSnafu { url: &self.url })?;
47
48        if !response.status().is_success() {
49            return Err(DicomWebError::HttpStatusFailure {
50                status_code: response.status(),
51            });
52        }
53
54        // Check if the response is a DICOM-JSON
55        let ct = response
56            .headers()
57            .get("Content-Type")
58            .ok_or(DicomWebError::MissingContentTypeHeader)?;
59        let media_type = MediaType::parse(ct.to_str().unwrap_or_default())
60            .map_err(|e| DicomWebError::ContentTypeParseFailed { source: e })?;
61
62        // Check if we have a DICOM-JSON or JSON content type
63        if media_type.essence() != MediaType::new(APPLICATION, JSON)
64            && media_type.essence()
65                != MediaType::from_parts(APPLICATION, Name::new_unchecked("dicom"), Some(JSON), &[])
66        {
67            return Err(DicomWebError::UnexpectedContentType {
68                content_type: ct.to_str().unwrap_or_default().to_string(),
69            });
70        }
71
72        Ok(response
73            .json::<Vec<DicomJson<InMemDicomObject>>>()
74            .await
75            .context(DeserializationFailedSnafu {})?
76            .into_iter()
77            .map(|dj| dj.into_inner())
78            .collect())
79    }
80}
81
82/// A builder type for WADO-RS file requests
83#[derive(Debug, Clone)]
84pub struct WadoFileRequest {
85    client: DicomWebClient,
86    url: String,
87}
88
89impl WadoFileRequest {
90    pub fn new(client: DicomWebClient, url: String) -> Self {
91        WadoFileRequest { client, url }
92    }
93
94    pub async fn run(
95        &self,
96    ) -> Result<
97        impl Stream<Item = Result<FileDicomObject<InMemDicomObject>, DicomWebError>>,
98        DicomWebError,
99    > {
100        let mut request = self.client.client.get(&self.url);
101
102        // Basic authentication
103        if let Some(username) = &self.client.username {
104            request = request.basic_auth(username, self.client.password.as_ref());
105        }
106        // Bearer token
107        else if let Some(bearer_token) = &self.client.bearer_token {
108            request = request.bearer_auth(bearer_token);
109        }
110
111        let response = request
112            .send()
113            .await
114            .context(RequestFailedSnafu { url: &self.url })?;
115
116        if !response.status().is_success() {
117            return Err(DicomWebError::HttpStatusFailure {
118                status_code: response.status(),
119            });
120        }
121
122        // Build the MultipartReader
123        let headers: Vec<(String, String)> = response
124            .headers()
125            .iter()
126            .map(|(k, v)| (k.to_string(), String::from(v.to_str().unwrap_or_default())))
127            .collect();
128
129        let stream = response.bytes_stream();
130        let reader = MultipartReader::from_stream_with_headers(stream, &headers)
131            .map_err(|source| DicomWebError::MultipartReaderFailed { source })?;
132
133        if reader.multipart_type != MultipartType::Related {
134            return Err(DicomWebError::UnexpectedMultipartType {
135                multipart_type: (reader.multipart_type),
136            });
137        }
138
139        Ok(reader.map(|item| {
140            let item = item.context(MultipartReaderFailedSnafu)?;
141            // Get the Content-Type header
142            let ct = item
143                .headers
144                .iter()
145                .find(|(k, _)| k.to_lowercase() == "content-type")
146                .map(|(_, v)| v.as_str())
147                .context(MissingContentTypeHeaderSnafu)?;
148            let media_type = MediaType::parse(ct)
149                .map_err(|e| DicomWebError::ContentTypeParseFailed { source: e })?;
150
151            // Check if we have a DICOM-JSON or JSON content type
152            if media_type.essence() != MediaType::new(APPLICATION, JSON)
153                && media_type.essence()
154                    != MediaType::from_parts(
155                        APPLICATION,
156                        Name::new_unchecked("dicom"),
157                        Some(JSON),
158                        &[],
159                    )
160            {
161                return Err(DicomWebError::UnexpectedContentType {
162                    content_type: ct.to_owned(),
163                });
164            }
165            from_reader(&*item.data).context(DicomReaderFailedSnafu)
166        }))
167    }
168}
169
170/// A builder type for WADO-RS single file requests
171pub struct WadoSingleFileRequest {
172    request: WadoFileRequest,
173}
174
175impl WadoSingleFileRequest {
176    pub async fn run(&self) -> Result<FileDicomObject<InMemDicomObject>, DicomWebError> {
177        // Run the request and get the first item of the stream
178        let mut stream = self.request.run().await?;
179        stream.next().await.context(EmptyResponseSnafu)?
180    }
181}
182
183/// A builder type for WADO-RS frames requests
184pub struct WadoFramesRequest {
185    client: DicomWebClient,
186    url: String,
187}
188
189impl WadoFramesRequest {
190    pub fn new(client: DicomWebClient, url: String) -> Self {
191        WadoFramesRequest { client, url }
192    }
193
194    pub async fn run(&self) -> Result<Vec<MultipartItem>, DicomWebError> {
195        let mut request = self.client.client.get(&self.url);
196
197        // Basic authentication
198        if let Some(username) = &self.client.username {
199            request = request.basic_auth(username, self.client.password.as_ref());
200        }
201        // Bearer token
202        else if let Some(bearer_token) = &self.client.bearer_token {
203            request = request.bearer_auth(bearer_token);
204        }
205
206        let response = request
207            .send()
208            .await
209            .context(RequestFailedSnafu { url: &self.url })?;
210
211        if !response.status().is_success() {
212            return Err(DicomWebError::HttpStatusFailure {
213                status_code: response.status(),
214            });
215        }
216
217        // Build the MultipartReader
218        let headers: Vec<(String, String)> = response
219            .headers()
220            .iter()
221            .map(|(k, v)| (k.to_string(), String::from(v.to_str().unwrap_or_default())))
222            .collect();
223        let stream = response.bytes_stream();
224        let mut reader = MultipartReader::from_stream_with_headers(stream, &headers)
225            .map_err(|source| DicomWebError::MultipartReaderFailed { source })?;
226
227        if reader.multipart_type != MultipartType::Related {
228            return Err(DicomWebError::UnexpectedMultipartType {
229                multipart_type: (reader.multipart_type),
230            });
231        }
232
233        let mut item_list = vec![];
234
235        while let Some(item) = reader.next().await {
236            let item = item.context(MultipartReaderFailedSnafu)?;
237            item_list.push(item);
238        }
239
240        Ok(item_list)
241    }
242}
243
244impl DicomWebClient {
245    /// Create a WADO-RS request to retrieve a specific study
246    pub fn retrieve_study(&self, study_instance_uid: &str) -> WadoFileRequest {
247        let url = format!("{}/studies/{}", self.wado_url, study_instance_uid);
248        WadoFileRequest::new(self.clone(), url)
249    }
250
251    /// Create a WADO-RS request to retrieve the metadata of a specific study
252    pub fn retrieve_study_metadata(&self, study_instance_uid: &str) -> WadoMetadataRequest {
253        let url = format!("{}/studies/{}/metadata", self.wado_url, study_instance_uid);
254        WadoMetadataRequest::new(self.clone(), url)
255    }
256
257    /// Create a WADO-RS request to retrieve a specific series
258    pub fn retrieve_series(
259        &self,
260        study_instance_uid: &str,
261        series_instance_uid: &str,
262    ) -> WadoFileRequest {
263        let base_url = &self.wado_url;
264        let url = format!("{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}",);
265        WadoFileRequest::new(self.clone(), url)
266    }
267
268    /// Create a WADO-RS request to retrieve the metadata of a specific series
269    pub fn retrieve_series_metadata(
270        &self,
271        study_instance_uid: &str,
272        series_instance_uid: &str,
273    ) -> WadoMetadataRequest {
274        let base_url = &self.wado_url;
275        let url = format!(
276            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/metadata"
277        );
278        WadoMetadataRequest::new(self.clone(), url)
279    }
280
281    /// Create a WADO-RS request to retrieve a specific instance
282    pub fn retrieve_instance(
283        &self,
284        study_instance_uid: &str,
285        series_instance_uid: &str,
286        sop_instance_uid: &str,
287    ) -> WadoSingleFileRequest {
288        let base_url = &self.wado_url;
289        let url = format!(
290            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances/{sop_instance_uid}",
291        );
292        WadoSingleFileRequest {
293            request: WadoFileRequest::new(self.clone(), url),
294        }
295    }
296
297    /// Create a WADO-RS request to retrieve the metadata of a specific instance
298    pub fn retrieve_instance_metadata(
299        &self,
300        study_instance_uid: &str,
301        series_instance_uid: &str,
302        sop_instance_uid: &str,
303    ) -> WadoMetadataRequest {
304        let base_url = &self.wado_url;
305        let url = format!(
306            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances/{sop_instance_uid}/metadata",
307        );
308        WadoMetadataRequest::new(self.clone(), url)
309    }
310
311    /// Create a WADO-RS request to retrieve specific frames inside an instance
312    pub fn retrieve_frames(
313        &self,
314        study_instance_uid: &str,
315        series_instance_uid: &str,
316        sop_instance_uid: &str,
317        framelist: &[u32],
318    ) -> WadoFramesRequest {
319        let framelist = framelist
320            .iter()
321            .map(|f| f.to_string())
322            .collect::<Vec<String>>()
323            .join(",");
324        let base_url = &self.wado_url;
325        let url = format!(
326            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances/{sop_instance_uid}/frames/{framelist}",
327        );
328        WadoFramesRequest::new(self.clone(), url)
329    }
330}