Skip to content

Commit

Permalink
[web] Implement DICOMweb WADO-RS
Browse files Browse the repository at this point in the history
  • Loading branch information
feliwir committed Mar 7, 2024
1 parent 42eacf9 commit 47ebd04
Show file tree
Hide file tree
Showing 6 changed files with 495 additions and 32 deletions.
38 changes: 36 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ members = [
"storescu",
"toimage",
"transfer-syntax-registry",
"ul", "web",
"ul",
"web",
]

# use edition 2021 resolver
Expand Down
10 changes: 8 additions & 2 deletions web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ edition = "2021"
dicom-core = { path = "../core", version = "0.6.3" }
dicom-json = { path = "../json/", version = "0.1.1" }
dicom-object = { path = "../object/", version = "0.6.3" }
reqwest = { version = "0.11.24", features = ["json"] }
dicom-transfer-syntax-registry = { path = "../transfer-syntax-registry", version = "0.6.2" }
dicom-pixeldata = { path = "../pixeldata/", version = "0.2.2" }
futures-util = "0.3.30"
mime = "0.3.17"
multipart-rs = "0.1.8"
reqwest = { version = "0.11.24", features = ["json", "stream"] }
serde_json = { version = "1.0.96", features = ["preserve_order"] }
snafu = "0.8.1"

[dev-dependencies]
dicom-dictionary-std = { path = "../dictionary-std", version = "0.6.1" }
tokio = { version = "1.36.0", features = ["macros"] }
wiremock = "0.6.0"
wiremock = "0.6.0"
135 changes: 128 additions & 7 deletions web/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use multipart_rs::MultipartType;
use reqwest::StatusCode;
use snafu::Snafu;

mod qido;
mod wado;

#[derive(Debug, Clone)]
struct DicomWebClient {
pub struct DicomWebClient {
wado_url: String,
qido_url: String,
stow_url: String,
Expand All @@ -24,6 +28,22 @@ pub enum DicomWebError {
RequestFailed { url: String, source: reqwest::Error },
#[snafu(display("Failed to deserialize response from server"))]
DeserializationFailed { source: reqwest::Error },
#[snafu(display("Failed to parse multipart response"))]
MultipartReaderFailed {
source: multipart_rs::MultipartError,
},
#[snafu(display("Failed to read DICOM object from multipart item"))]
DicomReaderFailed { source: dicom_object::ReadError },
#[snafu(display("HTTP status code indicates failure"))]
HttpStatusFailure { status_code: StatusCode },
#[snafu(display("Multipart item missing Content-Type header"))]
MissingContentTypeHeader,
#[snafu(display("Unexpected content type: {}", content_type))]
UnexpectedContentType { content_type: String },
#[snafu(display("Failed to parse content type: {}", source))]
ContentTypeParseFailed { source: mime::FromStrError },
#[snafu(display("Unexpected multipart type: {:?}", multipart_type))]
UnexpectedMultipartType { multipart_type: MultipartType },
}

impl DicomWebClient {
Expand Down Expand Up @@ -131,24 +151,125 @@ mod tests {
}

#[tokio::test]
async fn qido_test() {
async fn query_study_test() {
let mock_server = start_dicomweb_mock_server().await;

let client = DicomWebClient::with_single_url(&mock_server.uri());

// Perform QIDO-RS requests
let result = client.query_studies().run().await;
assert!(result.is_ok());
}

#[tokio::test]
async fn query_series_test() {
let mock_server = start_dicomweb_mock_server().await;
let client = DicomWebClient::with_single_url(&mock_server.uri());
// Perform QIDO-RS requests
let result = client.query_series().run().await;
assert!(result.is_ok());
}

#[tokio::test]
async fn query_instances_test() {
let mock_server = start_dicomweb_mock_server().await;
let client = DicomWebClient::with_single_url(&mock_server.uri());
// Perform QIDO-RS requests
let result = client.query_instances().run().await;
assert!(result.is_ok());
let result = client.query_series_in_study("1.1.1.1").run().await;
}

#[tokio::test]
async fn query_series_in_study_test() {
let mock_server = start_dicomweb_mock_server().await;
let client = DicomWebClient::with_single_url(&mock_server.uri());
// Perform QIDO-RS requests
let result = client
.query_series_in_study("1.2.276.0.89.300.10035584652.20181014.93645")
.run()
.await;
assert!(result.is_ok());
}

#[tokio::test]
async fn query_instances_in_series_test() {
let mock_server = start_dicomweb_mock_server().await;
let client = DicomWebClient::with_single_url(&mock_server.uri());
// Perform QIDO-RS requests
let result = client
.query_instances_in_series("1.2.276.0.89.300.10035584652.20181014.93645", "1.1.1.1")
.run()
.await;
assert!(result.is_ok());
}

#[tokio::test]
async fn retrieve_study_test() {
let mut client = DicomWebClient::with_single_url("http://localhost:8042/dicom-web");
client.set_basic_auth("orthanc", "orthanc");
// Perform WADO-RS requests
let result = client
.retrieve_study("1.2.276.0.89.300.10035584652.20181014.93645")
.run()
.await;
assert!(result.is_ok());
}

#[tokio::test]
async fn retrieve_study_metadata_test() {
let mut client = DicomWebClient::with_single_url("http://localhost:8042/dicom-web");
client.set_basic_auth("orthanc", "orthanc");
// Perform WADO-RS requests
let result = client
.retrieve_study_metadata("1.2.276.0.89.300.10035584652.20181014.93645")
.run()
.await;
assert!(result.is_ok());
}

#[tokio::test]
async fn retrieve_series_test() {
let mut client = DicomWebClient::with_single_url("http://localhost:8042/dicom-web");
client.set_basic_auth("orthanc", "orthanc");
// Perform WADO-RS requests
let result = client
.retrieve_series(
"1.2.276.0.89.300.10035584652.20181014.93645",
"1.2.392.200036.9125.3.1696751121028.64888163108.42362053",
)
.run()
.await;
assert!(result.is_ok());
}

#[tokio::test]
async fn retrieve_instance_test() {
let mut client = DicomWebClient::with_single_url("http://localhost:8042/dicom-web");
client.set_basic_auth("orthanc", "orthanc");
// Perform WADO-RS requests
let result = client
.retrieve_instance(
"1.2.276.0.89.300.10035584652.20181014.93645",
"1.2.392.200036.9125.3.1696751121028.64888163108.42362053",
"1.2.392.200036.9125.9.0.454007928.521494544.1883970570",
)
.run()
.await;
assert!(result.is_ok());
}

#[tokio::test]
async fn retrieve_frames_test() {
let mut client = DicomWebClient::with_single_url("http://localhost:8042/dicom-web");
client.set_basic_auth("orthanc", "orthanc");
// Perform WADO-RS requests
let result = client
.query_instances_in_series("1.1.1.1", "2.2.2.2")
.retrieve_frames(
"1.2.276.0.89.300.10035584652.20181014.93645",
"1.2.392.200036.9125.3.1696751121028.64888163108.42362053",
"1.2.392.200036.9125.9.0.454007928.521494544.1883970570",
&[1],
)
.run()
.await;
println!("{:?}", result);
assert!(result.is_ok());
}
}
51 changes: 31 additions & 20 deletions web/src/qido.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,30 @@ impl QidoRequest {
request = request.bearer_auth(self.client.bearer_token.as_ref().unwrap());
}

let text = self
.client
.client
.get(&self.url)
.query(&query)
let response = request
.send()
.await
.context(RequestFailedSnafu { url: &self.url })?
.text()
.await
.context(DeserializationFailedSnafu {})?;
println!("{}", text);
.context(RequestFailedSnafu { url: &self.url })?;

Ok(request
.send()
.await
.context(RequestFailedSnafu { url: &self.url })?
if !response.status().is_success() {
return Err(DicomWebError::HttpStatusFailure {
status_code: response.status(),
});
}

// Check if the response is a DICOM-JSON
let ct = response.headers().get("Content-Type");
if ct.is_none() {
return Err(DicomWebError::MissingContentTypeHeader);
}

if ct.unwrap() != "application/dicom+json" && ct.unwrap() != "application/json" {
return Err(DicomWebError::UnexpectedContentType {
content_type: ct.unwrap().to_str().unwrap().to_string(),
});
}

Ok(response
.json::<Vec<DicomJson<InMemDicomObject>>>()
.await
.context(DeserializationFailedSnafu {})?
Expand Down Expand Up @@ -120,25 +127,29 @@ impl QidoRequest {

impl DicomWebClient {
pub fn query_studies(&self) -> QidoRequest {
let url = format!("{}/studies", self.qido_url);
let base_url = &self.qido_url;
let url = format!("{base_url}/studies");

QidoRequest::new(self.clone(), url)
}

pub fn query_series(&self) -> QidoRequest {
let url = format!("{}/series", self.qido_url);
let base_url = &self.qido_url;
let url = format!("{base_url}/series");

QidoRequest::new(self.clone(), url)
}

pub fn query_series_in_study(&self, study_instance_uid: &str) -> QidoRequest {
let url = format!("{}/studies/{}/series", self.qido_url, study_instance_uid);
let base_url = &self.qido_url;
let url = format!("{base_url}/studies/{study_instance_uid}/series");

QidoRequest::new(self.clone(), url)
}

pub fn query_instances(&self) -> QidoRequest {
let url = format!("{}/instances", self.qido_url);
let base_url = &self.qido_url;
let url = format!("{base_url}/instances");

QidoRequest::new(self.clone(), url)
}
Expand All @@ -148,9 +159,9 @@ impl DicomWebClient {
study_instance_uid: &str,
series_instance_uid: &str,
) -> QidoRequest {
let base_url = &self.qido_url;
let url = format!(
"{}/studies/{}/series/{}/instances",
self.qido_url, study_instance_uid, series_instance_uid
"{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances",
);

QidoRequest::new(self.clone(), url)
Expand Down
Loading

0 comments on commit 47ebd04

Please sign in to comment.