引言
在 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(),
}
}
}
-
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>,
}
- 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(),
}
}
}
- 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