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        let response = request
78            .send()
79            .await
80            .context(RequestFailedSnafu { url: &self.url })?;
81
82        if !response.status().is_success() {
83            return Err(DicomWebError::HttpStatusFailure {
84                status_code: response.status(),
85            });
86        }
87
88        // Check if the response is a DICOM-JSON
89        let ct = response
90            .headers()
91            .get("Content-Type")
92            .ok_or(DicomWebError::MissingContentTypeHeader)?;
93        let media_type = MediaType::parse(ct.to_str().unwrap_or_default())
94            .map_err(|e| DicomWebError::ContentTypeParseFailed { source: e })?;
95
96        // Check if we have a DICOM-JSON or JSON content type
97        if media_type.essence() != MediaType::new(APPLICATION, JSON)
98            && media_type.essence()
99                != MediaType::from_parts(APPLICATION, Name::new_unchecked("dicom"), Some(JSON), &[])
100        {
101            return Err(DicomWebError::UnexpectedContentType {
102                content_type: ct.to_str().unwrap_or_default().to_string(),
103            });
104        }
105
106        Ok(response
107            .json::<Vec<DicomJson<InMemDicomObject>>>()
108            .await
109            .context(DeserializationFailedSnafu {})?
110            .into_iter()
111            .map(|dj| dj.into_inner())
112            .collect())
113    }
114
115    /// Set the maximum number of results to return. Will be passed as a query parameter.
116    /// This is useful for pagination.
117    pub fn with_limit(&mut self, limit: u32) -> &mut Self {
118        self.limit = Some(limit);
119        self
120    }
121
122    /// Set the offset of the results to return. Will be passed as a query parameter.
123    /// This is useful for pagination.
124    pub fn with_offset(&mut self, offset: u32) -> &mut Self {
125        self.offset = Some(offset);
126        self
127    }
128
129    /// Set the tags that should be queried. Will be passed as a query parameter.
130    pub fn with_includefields(&mut self, includefields: Vec<Tag>) -> &mut Self {
131        self.includefields = includefields;
132        self
133    }
134
135    /// Set whether fuzzy matching should be used. Will be passed as a query parameter.
136    pub fn with_fuzzymatching(&mut self, fuzzymatching: bool) -> &mut Self {
137        self.fuzzymatching = Some(fuzzymatching);
138        self
139    }
140
141    /// Add a filter to the query. Will be passed as a query parameter.
142    pub fn with_filter(&mut self, tag: Tag, value: String) -> &mut Self {
143        self.filters.push((tag, value));
144        self
145    }
146}
147
148impl DicomWebClient {
149    /// Create a DMWL-RS request to query all studies
150    pub fn query_modality_scheduled_procedure_steps(&self) -> MwlRequest {
151        let base_url = &self.qido_url;
152        let url = format!("{base_url}/modality-scheduled-procedure-steps");
153
154        MwlRequest::new(self.clone(), url)
155    }
156}