Skip to main content

dicom_web/
wado.rs

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