1use dicom_core::Tag;
3use dicom_json::DicomJson;
4use dicom_object::InMemDicomObject;
5
6use mediatype::{
7 names::{APPLICATION, JSON},
8 MediaType, Name,
9};
10use snafu::ResultExt;
11
12use crate::{DeserializationFailedSnafu, DicomWebClient, DicomWebError, RequestFailedSnafu};
13
14#[derive(Debug, Clone)]
17pub struct QidoRequest {
18 client: DicomWebClient,
19 url: String,
20
21 limit: Option<u32>,
22 offset: Option<u32>,
23 includefields: Vec<Tag>,
24 fuzzymatching: Option<bool>,
25 filters: Vec<(Tag, String)>,
26}
27
28impl QidoRequest {
29 fn new(client: DicomWebClient, url: String) -> Self {
30 QidoRequest {
31 client,
32 url,
33 limit: None,
34 offset: None,
35 includefields: vec![],
36 fuzzymatching: None,
37 filters: vec![],
38 }
39 }
40
41 pub async fn run(&self) -> Result<Vec<InMemDicomObject>, DicomWebError> {
43 let mut query: Vec<(String, String)> = vec![];
44 if let Some(limit) = self.limit {
45 query.push((String::from("limit"), limit.to_string()));
46 }
47 if let Some(offset) = self.offset {
48 query.push((String::from("offset"), offset.to_string()));
49 }
50 for include_field in self.includefields.iter() {
51 let radix_string = format!(
53 "{:04x}{:04x}",
54 include_field.group(),
55 include_field.element()
56 );
57
58 query.push((String::from("includefield"), radix_string));
59 }
60 for filter in self.filters.iter() {
61 query.push((filter.0.to_string(), filter.1.clone()));
62 }
63
64 let mut request = self.client.client.get(&self.url).query(&query);
65
66 if let Some(username) = &self.client.username {
68 request = request.basic_auth(username, self.client.password.as_ref());
69 }
70 else if let Some(bearer_token) = &self.client.bearer_token {
72 request = request.bearer_auth(bearer_token);
73 }
74
75 let response = request
76 .send()
77 .await
78 .context(RequestFailedSnafu { url: &self.url })?;
79
80 if !response.status().is_success() {
81 return Err(DicomWebError::HttpStatusFailure {
82 status_code: response.status(),
83 });
84 }
85
86 let ct = response
88 .headers()
89 .get("Content-Type")
90 .ok_or(DicomWebError::MissingContentTypeHeader)?;
91 let media_type = MediaType::parse(ct.to_str().unwrap_or_default())
92 .map_err(|e| DicomWebError::ContentTypeParseFailed { source: e })?;
93
94 if media_type.essence() != MediaType::new(APPLICATION, JSON)
96 && media_type.essence()
97 != MediaType::from_parts(APPLICATION, Name::new_unchecked("dicom"), Some(JSON), &[])
98 {
99 return Err(DicomWebError::UnexpectedContentType {
100 content_type: ct.to_str().unwrap_or_default().to_string(),
101 });
102 }
103
104 Ok(response
105 .json::<Vec<DicomJson<InMemDicomObject>>>()
106 .await
107 .context(DeserializationFailedSnafu {})?
108 .into_iter()
109 .map(|dj| dj.into_inner())
110 .collect())
111 }
112
113 pub fn with_limit(&mut self, limit: u32) -> &mut Self {
116 self.limit = Some(limit);
117 self
118 }
119
120 pub fn with_offset(&mut self, offset: u32) -> &mut Self {
123 self.offset = Some(offset);
124 self
125 }
126
127 pub fn with_includefields(&mut self, includefields: Vec<Tag>) -> &mut Self {
129 self.includefields = includefields;
130 self
131 }
132
133 pub fn with_fuzzymatching(&mut self, fuzzymatching: bool) -> &mut Self {
135 self.fuzzymatching = Some(fuzzymatching);
136 self
137 }
138
139 pub fn with_filter(&mut self, tag: Tag, value: String) -> &mut Self {
141 self.filters.push((tag, value));
142 self
143 }
144}
145
146impl DicomWebClient {
147 pub fn query_studies(&self) -> QidoRequest {
149 let base_url = &self.qido_url;
150 let url = format!("{base_url}/studies");
151
152 QidoRequest::new(self.clone(), url)
153 }
154
155 pub fn query_series(&self) -> QidoRequest {
157 let base_url = &self.qido_url;
158 let url = format!("{base_url}/series");
159
160 QidoRequest::new(self.clone(), url)
161 }
162
163 pub fn query_series_in_study(&self, study_instance_uid: &str) -> QidoRequest {
165 let base_url = &self.qido_url;
166 let url = format!("{base_url}/studies/{study_instance_uid}/series");
167
168 QidoRequest::new(self.clone(), url)
169 }
170
171 pub fn query_instances(&self) -> QidoRequest {
173 let base_url = &self.qido_url;
174 let url = format!("{base_url}/instances");
175
176 QidoRequest::new(self.clone(), url)
177 }
178
179 pub fn query_instances_in_series(
181 &self,
182 study_instance_uid: &str,
183 series_instance_uid: &str,
184 ) -> QidoRequest {
185 let base_url = &self.qido_url;
186 let url = format!(
187 "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances",
188 );
189
190 QidoRequest::new(self.clone(), url)
191 }
192}