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