Skip to main content

dicom_web/
asdo.rs

1//! Module for ASDO-RS requests
2//! See https://www.dicomstandard.org/News-dir/ftsup/docs/sups/sup248.pdf
3use dicom_core::ops::AttributeSelector;
4use dicom_json::DicomJson;
5use dicom_object::InMemDicomObject;
6
7use serde::{Deserialize, Serialize};
8use snafu::ResultExt;
9
10use crate::{
11    apply_auth_and_headers, selector_to_string, validate_dicom_json_content_type,
12    DeserializationFailedSnafu, DicomWebClient, DicomWebError, RequestFailedSnafu,
13};
14
15/// A builder type for ASDO-RS requests
16/// By default, the request is built with no filters and no destination.
17/// Destination must be set for the request to be valid, and will be passed as a query parameter.
18#[derive(Debug, Clone)]
19pub struct AsdoSendRequest {
20    client: DicomWebClient,
21    url: String,
22    destination: String,
23    // These are an extension for the ASDO-RS request, not part of the standard.
24    // They will be sent as a json body in the request, and can be used to provide authentication information for the destination.
25    username: Option<String>,
26    password: Option<String>,
27    token: Option<String>,
28
29    filters: Vec<(AttributeSelector, String)>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33struct AuthInfo {
34    username: Option<String>,
35    password: Option<String>,
36    token: Option<String>,
37}
38
39impl AsdoSendRequest {
40    fn new(client: DicomWebClient, url: String) -> Self {
41        AsdoSendRequest {
42            client,
43            url,
44            filters: vec![],
45            destination: String::new(),
46            username: None,
47            password: None,
48            token: None,
49        }
50    }
51
52    /// Execute the ASDO-RS request
53    pub async fn run(self) -> Result<InMemDicomObject, DicomWebError> {
54        let mut query: Vec<(String, String)> = vec![];
55        for (selector, value) in self.filters.iter() {
56            query.push((selector_to_string(&selector), value.clone()));
57        }
58
59        if self.destination.is_empty() {
60            return Err(DicomWebError::Other {
61                message: "Destination must be set for ASDO-RS request".to_string(),
62            });
63        }
64
65        query.push((String::from("destination"), self.destination.clone()));
66
67        let mut request = self.client.client.post(&self.url).query(&query);
68        // Forward the authentication information in the body of the request,
69        // since ASDO-RS does not have a standard way to provide authentication information for the destination.
70        if let (Some(username), Some(password)) = (&self.username, &self.password) {
71            request = request.json(&AuthInfo {
72                username: Some(username.clone()),
73                password: Some(password.clone()),
74                token: None,
75            });
76        } else if let Some(token) = &self.token {
77            request = request.json(&AuthInfo {
78                username: None,
79                password: None,
80                token: Some(token.clone()),
81            });
82        }
83
84        request = apply_auth_and_headers(request, &self.client);
85
86        let response = request
87            .send()
88            .await
89            .context(RequestFailedSnafu { url: &self.url })?;
90
91        if !response.status().is_success() {
92            return Err(DicomWebError::HttpStatusFailure {
93                status_code: response.status(),
94            });
95        }
96
97        // Check if the response is a DICOM-JSON
98        let ct = response
99            .headers()
100            .get("Content-Type")
101            .ok_or(DicomWebError::MissingContentTypeHeader)?;
102        validate_dicom_json_content_type(ct.to_str().unwrap_or_default())?;
103
104        Ok(response
105            .json::<DicomJson<InMemDicomObject>>()
106            .await
107            .context(DeserializationFailedSnafu {})?
108            .into_inner())
109    }
110
111    /// Add a filter to the query. Will be passed as a query parameter.
112    pub fn with_filter(mut self, selector: AttributeSelector, value: String) -> Self {
113        self.filters.push((selector, value));
114        self
115    }
116
117    /// Set the destination for the ASDO-RS request. Will be passed as a query parameter.
118    pub fn with_destination(mut self, destination: String) -> Self {
119        self.destination = destination;
120        self
121    }
122
123    pub fn with_basic_auth(mut self, username: String, password: String) -> Self {
124        self.username = Some(username);
125        self.password = Some(password);
126        self
127    }
128
129    pub fn with_bearer_token(mut self, token: String) -> Self {
130        self.token = Some(token);
131        self
132    }
133}
134
135#[derive(Debug, Clone)]
136pub struct AsdoStatusRequest {
137    client: DicomWebClient,
138    url: String,
139}
140
141impl AsdoStatusRequest {
142    fn new(client: DicomWebClient, url: String) -> Self {
143        AsdoStatusRequest { client, url }
144    }
145
146    pub async fn run(&self) -> Result<InMemDicomObject, DicomWebError> {
147        let request = self.client.client.get(&self.url);
148        let request = apply_auth_and_headers(request, &self.client);
149
150        let response = request
151            .send()
152            .await
153            .context(RequestFailedSnafu { url: &self.url })?;
154
155        if !response.status().is_success() {
156            return Err(DicomWebError::HttpStatusFailure {
157                status_code: response.status(),
158            });
159        }
160
161        // Check if the response is a DICOM-JSON
162        let ct = response
163            .headers()
164            .get("Content-Type")
165            .ok_or(DicomWebError::MissingContentTypeHeader)?;
166        validate_dicom_json_content_type(ct.to_str().unwrap_or_default())?;
167
168        Ok(response
169            .json::<DicomJson<InMemDicomObject>>()
170            .await
171            .context(DeserializationFailedSnafu {})?
172            .into_inner())
173    }
174}
175
176impl DicomWebClient {
177    /// Create an ASDO-RS request to send all studies
178    pub fn send_studies(&self, transaction_uid: &str) -> AsdoSendRequest {
179        let base_url = &self.qido_url;
180        let url = format!("{base_url}/studies/send-requests/{transaction_uid}");
181
182        AsdoSendRequest::new(self.clone(), url)
183    }
184
185    /// Create an ASDO-RS request to retrieve the status of a send request for all studies
186    pub fn send_studies_status(&self, transaction_uid: &str) -> AsdoStatusRequest {
187        let base_url = &self.qido_url;
188        let url = format!("{base_url}/studies/send-requests/{transaction_uid}");
189
190        AsdoStatusRequest::new(self.clone(), url)
191    }
192
193    /// Create an ASDO-RS request to send all series in a specific study
194    pub fn send_series_in_study(
195        &self,
196        study_instance_uid: &str,
197        transaction_uid: &str,
198    ) -> AsdoSendRequest {
199        let base_url = &self.qido_url;
200        let url = format!(
201            "{base_url}/studies/{study_instance_uid}/series/send-requests/{transaction_uid}"
202        );
203
204        AsdoSendRequest::new(self.clone(), url)
205    }
206
207    /// Create an ASDO-RS request to send all instances in a specific study
208    pub fn send_instances_in_study(
209        &self,
210        study_instance_uid: &str,
211        transaction_uid: &str,
212    ) -> AsdoSendRequest {
213        let base_url = &self.qido_url;
214        let url = format!(
215            "{base_url}/studies/{study_instance_uid}/instances/send-requests/{transaction_uid}"
216        );
217
218        AsdoSendRequest::new(self.clone(), url)
219    }
220
221    /// Create an ASDO-RS request to send all instances in a specific series
222    pub fn send_instances_in_series(
223        &self,
224        study_instance_uid: &str,
225        series_instance_uid: &str,
226        transaction_uid: &str,
227    ) -> AsdoSendRequest {
228        let base_url = &self.qido_url;
229        let url = format!(
230            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances/send-requests/{transaction_uid}",
231        );
232
233        AsdoSendRequest::new(self.clone(), url)
234    }
235}