dicom_web/
mwl.rs

1//! Module for MWL-RS requests
2//! See https://github.com/krotz-dieter/dicomweb-dmwl-mpps/blob/main/documents/sup246_05_DICOMwebModalityWorkflowService.docx
3//! Supplement 246: DICOMweb Modality Workflow Services
4use dicom_core::Tag;
5use dicom_json::DicomJson;
6use dicom_object::InMemDicomObject;
7
8use mediatype::{
9    names::{APPLICATION, JSON},
10    MediaType, Name,
11};
12use snafu::ResultExt;
13
14use crate::{DeserializationFailedSnafu, DicomWebClient, DicomWebError, RequestFailedSnafu};
15
16/// A builder type for MWL-RS requests
17/// By default, the request is built with no filters, no limit, and no offset.
18#[derive(Debug, Clone)]
19pub struct MwlRequest {
20    client: DicomWebClient,
21    url: String,
22
23    limit: Option<u32>,
24    offset: Option<u32>,
25    includefields: Vec<Tag>,
26    fuzzymatching: Option<bool>,
27    filters: Vec<(Tag, String)>,
28}
29
30impl MwlRequest {
31    pub fn new(client: DicomWebClient, url: String) -> Self {
32        MwlRequest {
33            client,
34            url,
35            limit: None,
36            offset: None,
37            includefields: vec![],
38            fuzzymatching: None,
39            filters: vec![],
40        }
41    }
42
43    /// Execute the MWL-RS request
44    pub async fn run(&self) -> Result<Vec<InMemDicomObject>, DicomWebError> {
45        let mut query: Vec<(String, String)> = vec![];
46        if let Some(limit) = self.limit {
47            query.push((String::from("limit"), limit.to_string()));
48        }
49        if let Some(offset) = self.offset {
50            query.push((String::from("offset"), offset.to_string()));
51        }
52        for include_field in self.includefields.iter() {
53            // Convert the tag to a radix string
54            let radix_string = format!(
55                "{:04x}{:04x}",
56                include_field.group(),
57                include_field.element()
58            );
59
60            query.push((String::from("includefield"), radix_string));
61        }
62        for filter in self.filters.iter() {
63            query.push((filter.0.to_string(), filter.1.clone()));
64        }
65
66        let mut request = self.client.client.get(&self.url).query(&query);
67
68        // Basic authentication
69        if let Some(username) = &self.client.username {
70            request = request.basic_auth(username, self.client.password.as_ref());
71        }
72        // Bearer token
73        else if let Some(bearer_token) = &self.client.bearer_token {
74            request = request.bearer_auth(bearer_token);
75        }
76
77        // Extra headers
78        for (key, value) in &self.client.extra_headers {
79            request = request.header(key, value);
80        }
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        // Check if the response is a DICOM-JSON
94        let ct = response
95            .headers()
96            .get("Content-Type")
97            .ok_or(DicomWebError::MissingContentTypeHeader)?;
98        let media_type = MediaType::parse(ct.to_str().unwrap_or_default())
99            .map_err(|e| DicomWebError::ContentTypeParseFailed { source: e })?;
100
101        // Check if we have a DICOM-JSON or JSON content type
102        if media_type.essence() != MediaType::new(APPLICATION, JSON)
103            && media_type.essence()
104                != MediaType::from_parts(APPLICATION, Name::new_unchecked("dicom"), Some(JSON), &[])
105        {
106            return Err(DicomWebError::UnexpectedContentType {
107                content_type: ct.to_str().unwrap_or_default().to_string(),
108            });
109        }
110
111        Ok(response
112            .json::<Vec<DicomJson<InMemDicomObject>>>()
113            .await
114            .context(DeserializationFailedSnafu {})?
115            .into_iter()
116            .map(|dj| dj.into_inner())
117            .collect())
118    }
119
120    /// Set the maximum number of results to return. Will be passed as a query parameter.
121    /// This is useful for pagination.
122    pub fn with_limit(&mut self, limit: u32) -> &mut Self {
123        self.limit = Some(limit);
124        self
125    }
126
127    /// Set the offset of the results to return. Will be passed as a query parameter.
128    /// This is useful for pagination.
129    pub fn with_offset(&mut self, offset: u32) -> &mut Self {
130        self.offset = Some(offset);
131        self
132    }
133
134    /// Set the tags that should be queried. Will be passed as a query parameter.
135    pub fn with_includefields(&mut self, includefields: Vec<Tag>) -> &mut Self {
136        self.includefields = includefields;
137        self
138    }
139
140    /// Set whether fuzzy matching should be used. Will be passed as a query parameter.
141    pub fn with_fuzzymatching(&mut self, fuzzymatching: bool) -> &mut Self {
142        self.fuzzymatching = Some(fuzzymatching);
143        self
144    }
145
146    /// Add a filter to the query. Will be passed as a query parameter.
147    pub fn with_filter(&mut self, tag: Tag, value: String) -> &mut Self {
148        self.filters.push((tag, value));
149        self
150    }
151}
152
153impl DicomWebClient {
154    /// Create a DMWL-RS request to query all studies
155    pub fn query_modality_scheduled_procedure_steps(&self) -> MwlRequest {
156        let base_url = &self.qido_url;
157        let url = format!("{base_url}/modality-scheduled-procedure-steps");
158
159        MwlRequest::new(self.clone(), url)
160    }
161}