dicom-web 0.5.0

A high-level API for accessing DICOMweb resources
Documentation
//! Module for WADO-RS requests
//! See https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.4
use dicom_json::DicomJson;
use dicom_object::{from_reader, FileDicomObject, InMemDicomObject};

use futures_util::{Stream, StreamExt};
use multipart_rs::{MultipartItem, MultipartReader, MultipartType};
use snafu::{OptionExt, ResultExt};

use crate::{
    apply_auth_and_headers, validate_dicom_json_content_type, validate_multipart_item_content_type,
    DeserializationFailedSnafu, DicomReaderFailedSnafu, DicomWebClient, DicomWebError,
    EmptyResponseSnafu, MissingContentTypeHeaderSnafu, MultipartReaderFailedSnafu,
    RequestFailedSnafu,
};

/// A builder type for WADO-RS metadata requests
#[derive(Debug, Clone)]
pub struct WadoMetadataRequest {
    client: DicomWebClient,
    url: String,
}

impl WadoMetadataRequest {
    fn new(client: DicomWebClient, url: String) -> Self {
        WadoMetadataRequest { client, url }
    }

    pub async fn run(&self) -> Result<Vec<InMemDicomObject>, DicomWebError> {
        let mut request = self.client.client.get(&self.url);
        request = apply_auth_and_headers(request, &self.client);

        let response = 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")
            .ok_or(DicomWebError::MissingContentTypeHeader)?;
        validate_dicom_json_content_type(ct.to_str().unwrap_or_default())?;

        Ok(response
            .json::<Vec<DicomJson<InMemDicomObject>>>()
            .await
            .context(DeserializationFailedSnafu {})?
            .into_iter()
            .map(|dj| dj.into_inner())
            .collect())
    }
}

/// A builder type for WADO-RS file requests
#[derive(Debug, Clone)]
pub struct WadoFileRequest {
    client: DicomWebClient,
    url: String,
}

impl WadoFileRequest {
    fn new(client: DicomWebClient, url: String) -> Self {
        WadoFileRequest { client, url }
    }

    pub async fn run(
        self,
    ) -> Result<
        impl Stream<Item = Result<FileDicomObject<InMemDicomObject>, DicomWebError>>,
        DicomWebError,
    > {
        let mut request = self.client.client.get(&self.url);
        request = apply_auth_and_headers(request, &self.client);

        let response = request
            .send()
            .await
            .context(RequestFailedSnafu { url: &self.url })?;

        if !response.status().is_success() {
            return Err(DicomWebError::HttpStatusFailure {
                status_code: response.status(),
            });
        }

        // Build the MultipartReader
        let headers: Vec<(String, String)> = response
            .headers()
            .iter()
            .map(|(k, v)| (k.to_string(), String::from(v.to_str().unwrap_or_default())))
            .collect();

        let stream = response.bytes_stream();
        let reader = MultipartReader::from_stream_with_headers(stream, &headers)
            .map_err(|source| DicomWebError::MultipartReaderFailed { source })?;

        if reader.multipart_type != MultipartType::Related {
            return Err(DicomWebError::UnexpectedMultipartType {
                multipart_type: reader.multipart_type,
            });
        }

        Ok(reader.map(|item| {
            let item = item.context(MultipartReaderFailedSnafu)?;
            // Get the Content-Type header
            let ct = item
                .headers
                .iter()
                .find(|(k, _)| k.to_lowercase() == "content-type")
                .map(|(_, v)| v.as_str())
                .context(MissingContentTypeHeaderSnafu)?;
            validate_multipart_item_content_type(ct)?;
            from_reader(&*item.data).context(DicomReaderFailedSnafu)
        }))
    }
}

/// A builder type for WADO-RS single file requests
pub struct WadoSingleFileRequest {
    request: WadoFileRequest,
}

impl WadoSingleFileRequest {
    pub async fn run(self) -> Result<FileDicomObject<InMemDicomObject>, DicomWebError> {
        // Run the request and get the first item of the stream
        let mut stream = self.request.run().await?;
        stream.next().await.context(EmptyResponseSnafu)?
    }
}

/// A builder type for WADO-RS frames requests
pub struct WadoFramesRequest {
    client: DicomWebClient,
    url: String,
}

impl WadoFramesRequest {
    fn new(client: DicomWebClient, url: String) -> Self {
        WadoFramesRequest { client, url }
    }

    pub async fn run(self) -> Result<Vec<MultipartItem>, DicomWebError> {
        let mut request = self.client.client.get(&self.url);
        request = apply_auth_and_headers(request, &self.client);

        let response = request
            .send()
            .await
            .context(RequestFailedSnafu { url: &self.url })?;

        if !response.status().is_success() {
            return Err(DicomWebError::HttpStatusFailure {
                status_code: response.status(),
            });
        }

        // Build the MultipartReader
        let headers: Vec<(String, String)> = response
            .headers()
            .iter()
            .map(|(k, v)| (k.to_string(), String::from(v.to_str().unwrap_or_default())))
            .collect();
        let stream = response.bytes_stream();
        let mut reader = MultipartReader::from_stream_with_headers(stream, &headers)
            .map_err(|source| DicomWebError::MultipartReaderFailed { source })?;

        if reader.multipart_type != MultipartType::Related {
            return Err(DicomWebError::UnexpectedMultipartType {
                multipart_type: reader.multipart_type,
            });
        }

        let mut item_list = vec![];

        while let Some(item) = reader.next().await {
            let item = item.context(MultipartReaderFailedSnafu)?;
            item_list.push(item);
        }

        Ok(item_list)
    }
}

impl DicomWebClient {
    /// Create a WADO-RS request to retrieve a specific study
    pub fn retrieve_study(&self, study_instance_uid: &str) -> WadoFileRequest {
        let url = format!("{}/studies/{}", self.wado_url, study_instance_uid);
        WadoFileRequest::new(self.clone(), url)
    }

    /// Create a WADO-RS request to retrieve the metadata of a specific study
    pub fn retrieve_study_metadata(&self, study_instance_uid: &str) -> WadoMetadataRequest {
        let url = format!("{}/studies/{}/metadata", self.wado_url, study_instance_uid);
        WadoMetadataRequest::new(self.clone(), url)
    }

    /// Create a WADO-RS request to retrieve a specific series
    pub fn retrieve_series(
        &self,
        study_instance_uid: &str,
        series_instance_uid: &str,
    ) -> WadoFileRequest {
        let base_url = &self.wado_url;
        let url = format!("{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}",);
        WadoFileRequest::new(self.clone(), url)
    }

    /// Create a WADO-RS request to retrieve the metadata of a specific series
    pub fn retrieve_series_metadata(
        &self,
        study_instance_uid: &str,
        series_instance_uid: &str,
    ) -> WadoMetadataRequest {
        let base_url = &self.wado_url;
        let url = format!(
            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/metadata"
        );
        WadoMetadataRequest::new(self.clone(), url)
    }

    /// Create a WADO-RS request to retrieve a specific instance
    pub fn retrieve_instance(
        &self,
        study_instance_uid: &str,
        series_instance_uid: &str,
        sop_instance_uid: &str,
    ) -> WadoSingleFileRequest {
        let base_url = &self.wado_url;
        let url = format!(
            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances/{sop_instance_uid}",
        );
        WadoSingleFileRequest {
            request: WadoFileRequest::new(self.clone(), url),
        }
    }

    /// Create a WADO-RS request to retrieve the metadata of a specific instance
    pub fn retrieve_instance_metadata(
        &self,
        study_instance_uid: &str,
        series_instance_uid: &str,
        sop_instance_uid: &str,
    ) -> WadoMetadataRequest {
        let base_url = &self.wado_url;
        let url = format!(
            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances/{sop_instance_uid}/metadata",
        );
        WadoMetadataRequest::new(self.clone(), url)
    }

    /// Create a WADO-RS request to retrieve specific frames inside an instance
    pub fn retrieve_frames(
        &self,
        study_instance_uid: &str,
        series_instance_uid: &str,
        sop_instance_uid: &str,
        framelist: &[u32],
    ) -> WadoFramesRequest {
        let framelist = framelist
            .iter()
            .map(|f| f.to_string())
            .collect::<Vec<String>>()
            .join(",");
        let base_url = &self.wado_url;
        let url = format!(
            "{base_url}/studies/{study_instance_uid}/series/{series_instance_uid}/instances/{sop_instance_uid}/frames/{framelist}",
        );
        WadoFramesRequest::new(self.clone(), url)
    }
}