引言

在 DICOM 医疗影像系统中,数据的准确性和一致性至关重要。本文将深入探讨如何使用 Rust 的类型系统来创建自定义的安全类型封装,以防止运行时错误并提高代码的可维护性。

BoundedString:长度限制字符串

BoundedString<N> 是一个泛型结构体,用于确保字符串长度不超过指定的 N 个字符。

FixedLengthString:固定长度字符串

FixedLengthString<N> 确保字符串长度恰好为 N 个字符,这在处理某些 DICOM 字段时非常有用。

DicomDateString:DICOM 日期格式

DICOM 标准要求日期字段使用 YYYYMMDD 格式,DicomDateString 类型专门用于处理这种格式。

类型系统的优势

1. 编译时保证

通过这些自定义类型,许多数据验证错误可以在编译时就被发现,而不是在运行时才暴露。

2. 明确的意图表达

类型名称本身就表达了数据的约束条件,使代码更具可读性和自文档化。

3. 防止错误传递

一旦数据通过类型验证,后续代码可以假设数据格式是正确的,无需重复验证。

具体实现:

1.dicom_dbtype.rs

use serde::{Deserialize, Serialize};
use snafu::Snafu;
use std::fmt;
use std::hash::Hash;

#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum BoundedStringError {
    #[snafu(display("String too long: {} > {}", len, max))]
    TooLong { max: usize, len: usize },
    #[snafu(display("String length is: {}  and  expected: {}", len, fixlen))]
    LengthError { fixlen: usize, len: usize },
}

type BoundedResult<T, E = BoundedStringError> = Result<T, E>;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
#[derive(Default)]
pub struct BoundedString<const N: usize> {
    value: String,
}

impl<const N: usize> fmt::Display for BoundedString<N> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.value)
    }
}

impl<const N: usize> BoundedString<N> {
    pub fn new(s: String) -> BoundedResult<BoundedString<N>> {
        if s.len() > N {
            Err(BoundedStringError::TooLong {
                max: N,
                len: s.len(),
            })
        } else {
            Ok(Self { value: s })
        }
    }
    pub fn from_str(s: &str) -> BoundedResult<BoundedString<N>> {
        if s.len() > N {
            Err(BoundedStringError::TooLong {
                max: N,
                len: s.len(),
            })
        } else {
            Ok(Self {
                value: s.to_string(),
            })
        }
    }
    pub fn from_string(s: &String) -> BoundedResult<BoundedString<N>> {
        if s.len() > N {
            Err(BoundedStringError::TooLong {
                max: N,
                len: s.len(),
            })
        } else {
            Ok(Self { value: s.clone() })
        }
    }
    pub fn as_str(&self) -> &str {
        &self.value
    }

    // 使用 deref 方式访问
    pub fn as_ref(&self) -> &String {
        &self.value
    }
}
impl<const N: usize> Hash for BoundedString<N> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.value.hash(state);
    }
}

impl<const N: usize> PartialEq for BoundedString<N> {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value
    }
}

impl<const N: usize> Eq for BoundedString<N> {}

impl<const N: usize> TryFrom<&str> for BoundedString<N> {
    type Error = BoundedStringError;
    fn try_from(s: &str) -> BoundedResult<Self> {
        BoundedString::from_str(s)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FixedLengthString<const N: usize> {
    pub(crate) value: String,
}

impl<const N: usize> Hash for FixedLengthString<N> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.value.hash(state);
    }
}

impl<const N: usize> PartialEq for FixedLengthString<N> {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value
    }
}

impl<const N: usize> Eq for FixedLengthString<N> {}

/// DICOM文件中的表示日期的字符串,格式为 YYYYMMDD, 长度为 8, 例如 "20231005"
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct DicomDateString {
    pub(crate) value: String,
}

// 为 DicomDateString 实现 Display trait
impl fmt::Display for DicomDateString {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.value)
    }
}

impl TryFrom<&str> for DicomDateString {
    type Error = BoundedStringError;
    fn try_from(s: &str) -> BoundedResult<Self> {
        // 使用 NaiveDate 验证日期格式和有效性
        chrono::NaiveDate::parse_from_str(&s, "%Y%m%d").map_err(|_| {
            BoundedStringError::LengthError {
                fixlen: 8,
                len: s.len(),
            }
        })?;
        Ok(Self {
            value: s.to_string(),
        })
    }
}

impl TryFrom<&String> for DicomDateString {
    type Error = BoundedStringError;
    fn try_from(s: &String) -> BoundedResult<Self> {
        // 使用 NaiveDate 验证日期格式和有效性
        chrono::NaiveDate::parse_from_str(&s, "%Y%m%d").map_err(|_| {
            BoundedStringError::LengthError {
                fixlen: 8,
                len: s.len(),
            }
        })?;
        Ok(Self { value: s.clone() })
    }
}

impl<const N: usize> FixedLengthString<N> {
    pub fn new(s: String) -> BoundedResult<FixedLengthString<N>> {
        if s.len() != N {
            Err(BoundedStringError::LengthError {
                fixlen: N,
                len: s.len(),
            })
        } else {
            Ok(Self { value: s })
        }
    }

    pub fn from_str(s: &str) -> BoundedResult<FixedLengthString<N>> {
        if s.len() != N {
            Err(BoundedStringError::LengthError {
                fixlen: N,
                len: s.len(),
            })
        } else {
            Ok(Self {
                value: s.to_string(),
            })
        }
    }

    pub fn from_string(s: &String) -> BoundedResult<FixedLengthString<N>> {
        if s.len() != N {
            Err(BoundedStringError::LengthError {
                fixlen: N,
                len: s.len(),
            })
        } else {
            Ok(Self { value: s.clone() })
        }
    }

    pub fn as_str(&self) -> &str {
        &self.value
    }
}

impl DicomDateString {
    pub fn as_str(&self) -> &str {
        self.value.as_str()
    }

    pub(crate) fn from_db(s: &str) -> Self {
        // 使用 NaiveDate 验证日期格式和有效性
        chrono::NaiveDate::parse_from_str(&s, "%Y%m%d")
            .map_err(|_| BoundedStringError::LengthError {
                fixlen: 8,
                len: s.len(),
            })
            .expect(
                format!(
                    "DicomDateString::make_from_db  only support YYYYMMDD format, but got {}",
                    s
                )
                .as_str(),
            );

        Self {
            value: s.to_string(),
        }
    }
}
  1. dicom_meta.rs

    此处对模型定义搞不清楚的,可以 参考:DICOM 基本概念介绍

use crate::dicom_dbtype::{BoundedString, DicomDateString, FixedLengthString};
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};

#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TransferStatus {
    NoNeedTransfer,
    Success,
    Failed,
}


#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DicomStoreMeta {
    #[serde(rename = "trace_id")]
    pub trace_id: FixedLengthString<36>,
    #[serde(rename = "worker_node_id")]
    pub worker_node_id: BoundedString<64>,
    #[serde(rename = "tenant_id")]
    pub tenant_id: BoundedString<64>,
    #[serde(rename = "patient_id")]
    pub patient_id: BoundedString<64>,
    #[serde(rename = "study_uid")]
    pub study_uid: BoundedString<64>,
    #[serde(rename = "series_uid")]
    pub series_uid: BoundedString<64>,
    #[serde(rename = "sop_uid")]
    pub sop_uid: BoundedString<64>,
    #[serde(rename = "file_size")]
    pub file_size: u32,
    #[serde(rename = "file_path")]
    pub file_path: BoundedString<512>,
    #[serde(rename = "transfer_syntax_uid")]
    pub transfer_syntax_uid: BoundedString<64>,
    #[serde(rename = "number_of_frames")]
    pub number_of_frames: i32,
    #[serde(rename = "created_time")]
    pub created_time: NaiveDateTime,
    #[serde(rename = "series_uid_hash")]
    pub series_uid_hash: BoundedString<20>,
    #[serde(rename = "study_uid_hash")]
    pub study_uid_hash: BoundedString<20>,
    #[serde(rename = "accession_number")]
    pub accession_number: BoundedString<16>,
    #[serde(rename = "target_ts")]
    pub target_ts: BoundedString<64>,
    #[serde(rename = "study_date")]
    pub study_date: NaiveDate,
    #[serde(rename = "transfer_status")]
    pub transfer_status: TransferStatus,
    #[serde(rename = "source_ip")]
    pub source_ip: BoundedString<24>,
    #[serde(rename = "source_ae")]
    pub source_ae: BoundedString<64>,
}

impl Hash for DicomStoreMeta {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.tenant_id.hash(state);
        self.patient_id.hash(state);
        self.study_uid.hash(state);
        self.series_uid.hash(state);
        self.sop_uid.hash(state);
    }
}
impl PartialEq for DicomStoreMeta {
    fn eq(&self, other: &Self) -> bool {
        self.tenant_id == other.tenant_id
            && self.patient_id == other.patient_id
            && self.study_uid == other.study_uid
            && self.series_uid == other.series_uid
            && self.sop_uid == other.sop_uid
    }
}
impl Eq for DicomStoreMeta {}


#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DicomJsonMeta {
    #[serde(rename = "tenant_id")]
    pub tenant_id: BoundedString<64>,
    #[serde(rename = "study_uid")]
    pub study_uid: BoundedString<64>,
    #[serde(rename = "series_uid")]
    pub series_uid: BoundedString<64>,
    #[serde(rename = "study_uid_hash")]
    pub study_uid_hash: BoundedString<20>,
    #[serde(rename = "series_uid_hash")]
    pub series_uid_hash: BoundedString<20>,
    #[serde(rename = "study_date_origin")]
    pub study_date_origin: DicomDateString,
    #[serde(rename = "flag_time")]
    pub flag_time: NaiveDateTime,
    #[serde(rename = "created_time")]
    pub created_time: NaiveDateTime,
    #[serde(rename = "json_status")]
    pub json_status: i32,
    #[serde(rename = "retry_times")]
    pub retry_times: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DicomStateMeta {
    #[serde(rename = "tenant_id")]
    pub tenant_id: BoundedString<64>,
    #[serde(rename = "patient_id")]
    pub patient_id: BoundedString<64>,
    #[serde(rename = "study_uid")]
    pub study_uid: BoundedString<64>,
    #[serde(rename = "series_uid")]
    pub series_uid: BoundedString<64>,
    #[serde(rename = "study_uid_hash")]
    pub study_uid_hash: BoundedString<20>,
    #[serde(rename = "series_uid_hash")]
    pub series_uid_hash: BoundedString<20>,
    #[serde(rename = "study_date_origin")]
    pub study_date_origin: DicomDateString,

    #[serde(rename = "patient_name")]
    pub patient_name: Option<BoundedString<64>>,
    #[serde(rename = "patient_sex")]
    pub patient_sex: Option<BoundedString<1>>,
    #[serde(rename = "patient_birth_date")]
    pub patient_birth_date: Option<NaiveDate>,
    #[serde(rename = "patient_birth_time")]
    pub patient_birth_time: Option<NaiveTime>,
    #[serde(rename = "patient_age")]
    pub patient_age: Option<BoundedString<16>>,
    #[serde(rename = "patient_size")]
    pub patient_size: Option<f64>,
    #[serde(rename = "patient_weight")]
    pub patient_weight: Option<f64>,

    #[serde(rename = "study_date")]
    pub study_date: NaiveDate,
    #[serde(rename = "study_time")]
    pub study_time: Option<NaiveTime>,
    #[serde(rename = "accession_number")]
    pub accession_number: BoundedString<16>,
    #[serde(rename = "study_id")]
    pub study_id: Option<BoundedString<16>>,
    #[serde(rename = "study_description")]
    pub study_description: Option<BoundedString<64>>,

    #[serde(rename = "modality")]
    pub modality: Option<BoundedString<16>>,
    #[serde(rename = "series_number")]
    pub series_number: Option<i32>,
    #[serde(rename = "series_date")]
    pub series_date: Option<NaiveDate>,
    #[serde(rename = "series_time")]
    pub series_time: Option<NaiveTime>,
    #[serde(rename = "series_description")]
    pub series_description: Option<BoundedString<256>>,
    #[serde(rename = "body_part_examined")]
    pub body_part_examined: Option<BoundedString<64>>,
    #[serde(rename = "protocol_name")]
    pub protocol_name: Option<BoundedString<64>>,
    #[serde(rename = "series_related_instances")]
    pub series_related_instances: Option<i32>,
    #[serde(rename = "created_time")]
    pub created_time: NaiveDateTime,
    #[serde(rename = "updated_time")]
    pub updated_time: NaiveDateTime,
}

impl DicomStateMeta {
    pub fn unique_key(&self) -> (String, String, String, String) {
        (
            self.tenant_id.as_str().to_string(),
            self.patient_id.as_str().to_string(),
            self.study_uid.as_str().to_string(),
            self.series_uid.as_str().to_string(),
        )
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DicomImageMeta {
    #[serde(rename = "tenant_id")]
    pub tenant_id: BoundedString<64>,

    #[serde(rename = "patient_id")]
    pub patient_id: BoundedString<64>,

    #[serde(rename = "study_uid")]
    pub study_uid: BoundedString<64>,

    #[serde(rename = "series_uid")]
    pub series_uid: BoundedString<64>,

    #[serde(rename = "sop_uid")]
    pub sop_uid: BoundedString<64>,

    #[serde(rename = "study_uid_hash")]
    pub study_uid_hash: BoundedString<20>,

    #[serde(rename = "series_uid_hash")]
    pub series_uid_hash: BoundedString<20>,

    #[serde(rename = "instance_number")]
    pub instance_number: Option<i32>,

    #[serde(rename = "content_date")]
    pub content_date: Option<DicomDateString>,

    #[serde(rename = "content_time")]
    pub content_time: Option<NaiveTime>,

    #[serde(rename = "image_type")]
    pub image_type: Option<BoundedString<128>>,

    #[serde(rename = "image_orientation_patient")]
    pub image_orientation_patient: Option<BoundedString<128>>,

    #[serde(rename = "image_position_patient")]
    pub image_position_patient: Option<BoundedString<64>>,

    #[serde(rename = "slice_thickness")]
    pub slice_thickness: Option<f64>,

    #[serde(rename = "spacing_between_slices")]
    pub spacing_between_slices: Option<f64>,

    #[serde(rename = "slice_location")]
    pub slice_location: Option<f64>,

    #[serde(rename = "samples_per_pixel")]
    pub samples_per_pixel: Option<i32>,

    #[serde(rename = "photometric_interpretation")]
    pub photometric_interpretation: Option<BoundedString<32>>,

    #[serde(rename = "width")]
    pub width: Option<i32>,

    #[serde(rename = "columns")]
    pub columns: Option<i32>,

    #[serde(rename = "bits_allocated")]
    pub bits_allocated: Option<i32>,

    #[serde(rename = "bits_stored")]
    pub bits_stored: Option<i32>,

    #[serde(rename = "high_bit")]
    pub high_bit: Option<i32>,

    #[serde(rename = "pixel_representation")]
    pub pixel_representation: Option<i32>,

    #[serde(rename = "rescale_intercept")]
    pub rescale_intercept: Option<f64>,

    #[serde(rename = "rescale_slope")]
    pub rescale_slope: Option<f64>,

    #[serde(rename = "rescale_type")]
    pub rescale_type: Option<BoundedString<64>>,

    #[serde(rename = "window_center")]
    pub window_center: Option<BoundedString<64>>,

    #[serde(rename = "window_width")]
    pub window_width: Option<BoundedString<64>>,

    #[serde(rename = "transfer_syntax_uid")]
    pub transfer_syntax_uid: BoundedString<64>,

    #[serde(rename = "pixel_data_location")]
    pub pixel_data_location: Option<BoundedString<512>>,

    #[serde(rename = "thumbnail_location")]
    pub thumbnail_location: Option<BoundedString<512>>,

    #[serde(rename = "sop_class_uid")]
    pub sop_class_uid: BoundedString<64>,

    #[serde(rename = "image_status")]
    pub image_status: Option<BoundedString<32>>,

    #[serde(rename = "space_size")]
    pub space_size: Option<u32>,

    #[serde(rename = "created_time")]
    pub created_time: Option<NaiveDateTime>,

    #[serde(rename = "updated_time")]
    pub updated_time: Option<NaiveDateTime>,
}
  1. dicom_mysql_types.rs

use crate::dicom_dbtype::{BoundedString, DicomDateString};
use mysql::prelude::*;
use mysql::Value;

impl<const N: usize> From<BoundedString<N>> for Value {
    fn from(bounded_string: BoundedString<N>) -> Self {
        Value::from(bounded_string.as_str())
    }
}
impl<const N: usize> From<String> for BoundedString<N> {
    fn from(value: String) -> Self {
        BoundedString::<N>::new(value.as_str().parse().unwrap()).unwrap()
    }
}
impl<const N: usize> FromValue for BoundedString<N> {
    type Intermediate = String; // 恢复为 String

    fn from_value(v: Value) -> BoundedString<N> {
        let s = String::from_value(v);
        // 直接使用 try_from 并处理错误
        BoundedString::<N>::try_from(s).unwrap_or_else(|_| BoundedString::<N>::default())
    }
}

impl From<DicomDateString> for Value {
    fn from(dicom_date_string: DicomDateString) -> Self {
        Value::from(dicom_date_string.as_str())
    }
}

impl From<String> for DicomDateString {
    fn from(value: String) -> Self {
        DicomDateString::from_db(value.as_str())
    }
}
impl FromValue for DicomDateString {
    type Intermediate = String; // 恢复为 String

    fn from_value(v: Value) -> DicomDateString {
        let s = String::from_value(v);
        // 直接使用 try_from 并处理错误
        DicomDateString::try_from(s).unwrap_or_else(|_| DicomDateString::default())
    }
}


impl Default for DicomDateString {
    fn default() -> Self {
        DicomDateString {
            value: "00000000".to_string(),
        }
    }
}
  1. dicom_postgres_types.rs
use crate::dicom_dbtype::{BoundedString, DicomDateString};
use postgres_types::private::BytesMut;
use postgres_types::{FromSql, IsNull, ToSql, Type};
use std::error::Error;

impl<const N: usize> ToSql for BoundedString<N> {
    fn to_sql(&self, ty: &Type, out: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>>
    where
        Self: Sized,
    {
        <&str as ToSql>::to_sql(&self.as_str(), ty, out)
    }

    fn accepts(ty: &Type) -> bool {
        <&str as ToSql>::accepts(ty)
    }

    fn to_sql_checked(
        &self,
        ty: &Type,
        out: &mut BytesMut,
    ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
        <&str as ToSql>::to_sql_checked(&self.as_str(), ty, out)
    }
}

impl<const N: usize> FromSql<'_> for BoundedString<N> {
    fn from_sql(ty: &Type, raw: &[u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
        let str_val = <&str as FromSql>::from_sql(ty, raw)?;
        Ok(BoundedString::try_from(str_val.to_string())
            .map_err(|e| format!("Failed to create BoundedString FromSql: {}", e))?)
    }

    fn accepts(ty: &Type) -> bool {
        <&str as FromSql>::accepts(ty)
    }
}
impl ToSql for DicomDateString {
    fn to_sql(&self, ty: &Type, out: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>>
    where
        Self: Sized,
    {
        <&str as ToSql>::to_sql(&self.as_str(), ty, out)
    }

    fn accepts(ty: &Type) -> bool {
        <&str as ToSql>::accepts(ty)
    }

    fn to_sql_checked(
        &self,
        ty: &Type,
        out: &mut BytesMut,
    ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
        <&str as ToSql>::to_sql_checked(&self.as_str(), ty, out)
    }
}
impl FromSql<'_> for DicomDateString {
    fn from_sql(ty: &Type, raw: &[u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
        let str_val = <&str as FromSql>::from_sql(ty, raw)?;
        Ok(DicomDateString::try_from(str_val.to_string())
            .map_err(|e| format!("Failed to create DicomDateString FromSql: {}", e))?)
    }

    fn accepts(ty: &Type) -> bool {
        <&str as FromSql>::accepts(ty)
    }
}

总结

通过创建这些自定义类型,我们能够在 Rust 的类型系统中编码业务规则,从而构建更安全、更可靠的 DICOM 医疗影像系统。这种方法不仅提高了代码质量,还增强了系统的可维护性和可扩展性。

GoTo Summary : how-to-build-cloud-dicom

GoTo Database-Sign : how-to-build-cloud-dicom:Part 1 Database Design