1use serde::{Deserialize, Serialize};
2
3use crate::WsiDicomError;
4
5#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
7pub struct DicomMetadata {
8 pub patient_name: Option<String>,
9 pub patient_id: Option<String>,
10 pub patient_birth_date: Option<String>,
11 pub patient_sex: Option<String>,
12 pub accession_number: Option<String>,
13 pub study_instance_uid: Option<String>,
14 pub study_id: Option<String>,
15 pub study_date: Option<String>,
16 pub study_time: Option<String>,
17 pub study_description: Option<String>,
18 pub referring_physician_name: Option<String>,
19 pub laterality: Option<String>,
20 pub manufacturer: Option<String>,
21 pub manufacturer_model_name: Option<String>,
22 pub device_serial_number: Option<String>,
23 pub software_versions: Option<String>,
24 pub content_date: Option<String>,
25 pub content_time: Option<String>,
26 pub acquisition_date_time: Option<String>,
27 pub container_identifier: Option<String>,
28 pub specimen_identifier: Option<String>,
29 pub specimen_description: Option<String>,
30 pub imaged_volume_depth_mm: Option<f64>,
31 pub focus_method: Option<String>,
32}
33
34impl DicomMetadata {
35 pub fn research_placeholder() -> Self {
36 Self {
37 patient_name: Some("RESEARCH^PLACEHOLDER".into()),
38 patient_id: Some("RESEARCH".into()),
39 patient_birth_date: Some(String::new()),
40 patient_sex: Some(String::new()),
41 accession_number: Some("RESEARCH".into()),
42 study_id: Some("1".into()),
43 study_date: Some("19700101".into()),
44 study_time: Some("000000".into()),
45 study_description: Some("Research placeholder WSI export".into()),
46 referring_physician_name: Some(String::new()),
47 laterality: Some(String::new()),
48 manufacturer: Some("wsi-dicom".into()),
49 manufacturer_model_name: Some("wsi-dicom".into()),
50 device_serial_number: Some("RESEARCH".into()),
51 software_versions: Some(env!("CARGO_PKG_VERSION").into()),
52 content_date: Some("19700101".into()),
53 content_time: Some("000000".into()),
54 acquisition_date_time: Some("19700101000000".into()),
55 container_identifier: Some("RESEARCH-CONTAINER".into()),
56 specimen_identifier: Some("RESEARCH-SPECIMEN".into()),
57 specimen_description: Some("Research placeholder specimen".into()),
58 imaged_volume_depth_mm: Some(0.001),
59 focus_method: Some("AUTO".into()),
60 study_instance_uid: None,
61 }
62 }
63
64 pub fn from_fhir_r4_bundle(value: &serde_json::Value) -> Result<Self, WsiDicomError> {
65 let mut metadata = Self::default();
66 let resources = fhir_resources(value)?;
67 for resource in resources {
68 match resource
69 .get("resourceType")
70 .and_then(serde_json::Value::as_str)
71 {
72 Some("Patient") => map_fhir_patient(resource, &mut metadata),
73 Some("Specimen") => map_fhir_specimen(resource, &mut metadata),
74 Some("ServiceRequest") => map_fhir_service_request(resource, &mut metadata),
75 Some("DiagnosticReport") => map_fhir_diagnostic_report(resource, &mut metadata),
76 _ => {}
77 }
78 }
79 metadata.validate_strict()?;
80 Ok(metadata)
81 }
82
83 pub fn validate_strict(&self) -> Result<(), WsiDicomError> {
84 if self.patient_id.as_deref().unwrap_or_default().is_empty() {
85 return Err(WsiDicomError::Metadata {
86 reason: "strict metadata requires patient_id".into(),
87 });
88 }
89 if self.patient_name.as_deref().unwrap_or_default().is_empty() {
90 return Err(WsiDicomError::Metadata {
91 reason: "strict metadata requires patient_name".into(),
92 });
93 }
94 Ok(())
95 }
96}
97
98#[derive(Debug, Clone, PartialEq)]
100pub enum MetadataSource {
101 Strict(Box<DicomMetadata>),
102 ResearchPlaceholder,
103 FhirR4Bundle(serde_json::Value),
104}
105
106impl MetadataSource {
107 pub(crate) fn resolve(&self) -> Result<DicomMetadata, WsiDicomError> {
108 match self {
109 Self::Strict(metadata) => {
110 metadata.validate_strict()?;
111 Ok(metadata.as_ref().clone())
112 }
113 Self::ResearchPlaceholder => Ok(DicomMetadata::research_placeholder()),
114 Self::FhirR4Bundle(bundle) => DicomMetadata::from_fhir_r4_bundle(bundle),
115 }
116 }
117}
118
119fn fhir_resources(value: &serde_json::Value) -> Result<Vec<&serde_json::Value>, WsiDicomError> {
120 match value
121 .get("resourceType")
122 .and_then(serde_json::Value::as_str)
123 {
124 Some("Bundle") => Ok(value
125 .get("entry")
126 .and_then(serde_json::Value::as_array)
127 .ok_or_else(|| WsiDicomError::Metadata {
128 reason: "FHIR Bundle is missing entry array".into(),
129 })?
130 .iter()
131 .filter_map(|entry| entry.get("resource"))
132 .collect()),
133 Some(_) => Ok(vec![value]),
134 None => Err(WsiDicomError::Metadata {
135 reason: "FHIR JSON is missing resourceType".into(),
136 }),
137 }
138}
139
140fn map_fhir_patient(resource: &serde_json::Value, metadata: &mut DicomMetadata) {
141 metadata.patient_id = first_identifier(resource).or_else(|| json_string(resource, "/id"));
142 metadata.patient_name = resource
143 .get("name")
144 .and_then(serde_json::Value::as_array)
145 .and_then(|names| names.first())
146 .and_then(fhir_human_name_to_pn);
147 metadata.patient_birth_date =
148 json_string(resource, "/birthDate").map(|date| date.replace('-', ""));
149 metadata.patient_sex =
150 json_string(resource, "/gender").and_then(|gender| match gender.as_str() {
151 "male" => Some("M".to_string()),
152 "female" => Some("F".to_string()),
153 "other" => Some("O".to_string()),
154 "unknown" => Some("U".to_string()),
155 _ => None,
156 });
157}
158
159fn map_fhir_specimen(resource: &serde_json::Value, metadata: &mut DicomMetadata) {
160 metadata.specimen_identifier = json_string(resource, "/accessionIdentifier/value")
161 .or_else(|| first_identifier(resource))
162 .or_else(|| json_string(resource, "/id"));
163 if metadata.container_identifier.is_none() {
164 metadata.container_identifier = metadata.specimen_identifier.clone();
165 }
166 metadata.specimen_description = json_string(resource, "/type/text");
167}
168
169fn map_fhir_service_request(resource: &serde_json::Value, metadata: &mut DicomMetadata) {
170 metadata.accession_number = first_identifier(resource)
171 .or_else(|| json_string(resource, "/requisition/value"))
172 .or_else(|| json_string(resource, "/id"));
173 if metadata.study_description.is_none() {
174 metadata.study_description = json_string(resource, "/code/text");
175 }
176}
177
178fn map_fhir_diagnostic_report(resource: &serde_json::Value, metadata: &mut DicomMetadata) {
179 if metadata.study_id.is_none() {
180 metadata.study_id = first_identifier(resource).or_else(|| json_string(resource, "/id"));
181 }
182 metadata.study_description = json_string(resource, "/code/text");
183}
184
185fn first_identifier(resource: &serde_json::Value) -> Option<String> {
186 resource
187 .get("identifier")
188 .and_then(serde_json::Value::as_array)
189 .and_then(|ids| ids.first())
190 .and_then(|id| json_string(id, "/value"))
191}
192
193fn fhir_human_name_to_pn(name: &serde_json::Value) -> Option<String> {
194 let family = name.get("family").and_then(serde_json::Value::as_str)?;
195 let given = name
196 .get("given")
197 .and_then(serde_json::Value::as_array)
198 .map(|values| {
199 values
200 .iter()
201 .filter_map(serde_json::Value::as_str)
202 .collect::<Vec<_>>()
203 .join(" ")
204 })
205 .unwrap_or_default();
206 if given.is_empty() {
207 Some(family.to_string())
208 } else {
209 Some(format!("{family}^{given}"))
210 }
211}
212
213fn json_string(value: &serde_json::Value, pointer: &str) -> Option<String> {
214 value
215 .pointer(pointer)
216 .and_then(serde_json::Value::as_str)
217 .filter(|s| !s.is_empty())
218 .map(ToOwned::to_owned)
219}