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 for (key, value) in &self.client.extra_headers {
77 request = request.header(key, value);
78 }
79
80 let response = request
81 .send()
82 .await
83 .context(RequestFailedSnafu { url: &self.url })?;
84
85 if !response.status().is_success() {
86 return Err(DicomWebError::HttpStatusFailure {
87 status_code: response.status(),
88 });
89 }
90
91 let ct = response
93 .headers()
94 .get("Content-Type")
95 .ok_or(DicomWebError::MissingContentTypeHeader)?;
96 let media_type = MediaType::parse(ct.to_str().unwrap_or_default())
97 .map_err(|e| DicomWebError::ContentTypeParseFailed { source: e })?;
98
99 if media_type.essence() != MediaType::new(APPLICATION, JSON)
101 && media_type.essence()
102 != MediaType::from_parts(APPLICATION, Name::new_unchecked("dicom"), Some(JSON), &[])
103 {
104 return Err(DicomWebError::UnexpectedContentType {
105 content_type: ct.to_str().unwrap_or_default().to_string(),
106 });
107 }
108
109 Ok(response
110 .json::<Vec<DicomJson<InMemDicomObject>>>()
111 .await
112 .context(DeserializationFailedSnafu {})?
113 .into_iter()
114 .map(|dj| dj.into_inner())
115 .collect())
116 }
117
118 pub fn with_limit(&mut self, limit: u32) -> &mut Self {
121 self.limit = Some(limit);
122 self
123 }
124
125 pub fn with_offset(&mut self, offset: u32) -> &mut Self {
128 self.offset = Some(offset);
129 self
130 }
131
132 pub fn with_includefields(&mut self, includefields: Vec<Tag>) -> &mut Self {
134 self.includefields = includefields;
135 self
136 }
137
138 pub fn with_fuzzymatching(&mut self, fuzzymatching: bool) -> &mut Self {
140 self.fuzzymatching = Some(fuzzymatching);
141 self
142 }
143
144 pub fn with_filter(&mut self, tag: Tag, value: String) -> &mut Self {
146 self.filters.push((tag, value));
147 self
148 }
149}
150
151impl DicomWebClient {
152 pub fn query_studies(&self) -> QidoRequest {
154 let base_url = &self.qido_url;
155 let url = format!("{base_url}/studies");
156
157 QidoRequest::new(self.clone(), url)
158 }
159
160 pub fn query_series(&self) -> QidoRequest {
162 let base_url = &self.qido_url;
163 let url = format!("{base_url}/series");
164
165 QidoRequest::new(self.clone(), url)
166 }
167
168 pub fn query_series_in_study(&self, study_instance_uid: &str) -> QidoRequest {
170 let base_url = &self.qido_url;
171 let url = format!("{base_url}/studies/{study_instance_uid}/series");
172
173 QidoRequest::new(self.clone(), url)
174 }
175
176 pub fn query_instances(&self) -> QidoRequest {
178 let base_url = &self.qido_url;
179 let url = format!("{base_url}/instances");
180
181 QidoRequest::new(self.clone(), url)
182 }
183
184 pub fn query_instances_in_series(
186 &self,
187 study_instance_uid: &str,
188 series_instance_uid: &str,
189 ) -> QidoRequest {
190 let base_url = &self.qido_url;
191 let url = format!(
192 "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances",
193 );
194
195 QidoRequest::new(self.clone(), url)
196 }
197}