jmix_rs/types/
mod.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5/// Complete JMIX envelope containing all components
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Envelope {
8    pub manifest: Manifest,
9    pub metadata: Metadata,
10    pub audit: Audit,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub manifest_jws: Option<String>, // JWS content if present
13}
14
15/// JMIX manifest.json structure
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Manifest {
18    pub version: String,
19    pub id: String,
20    pub timestamp: String,
21    pub sender: Entity,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub requester: Option<Entity>,
24    pub receiver: Vec<Entity>,
25    pub security: Security,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub extensions: Option<Extensions>,
28}
29
30/// Entity information (sender, requester, receiver)
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Entity {
33    pub name: Option<String>,
34    pub id: String,
35    pub contact: Contact,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub assertion: Option<Assertion>,
38}
39
40/// Contact information - can be string (email) or structured
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(untagged)]
43pub enum Contact {
44    Email(String),
45    Point(ContactPoint),
46}
47
48/// Structured contact information
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ContactPoint {
51    pub system: String,
52    pub value: String,
53}
54
55/// Cryptographic assertion for entities
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct Assertion {
58    pub signing_key: SigningKey,
59    pub key_reference: Option<String>,
60    pub signed_fields: Vec<String>,
61    pub signature: String,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub expires_at: Option<String>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub directory_attestation: Option<DirectoryAttestation>,
66}
67
68/// Signing key information
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct SigningKey {
71    pub alg: String, // \"Ed25519\"
72    pub public_key: String,
73    pub fingerprint: String, // SHA256:...
74}
75
76/// Directory attestation for PKI verification
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct DirectoryAttestation {
79    pub provider: String,
80    pub attestation_signature: String,
81    pub attestation_timestamp: String,
82    pub attestation_public_key: String,
83}
84
85/// Security information in manifest
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Security {
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub classification: Option<String>,
90    pub payload_hash: String, // sha256:...
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub jws: Option<JwsReference>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub signature: Option<LegacySignature>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub encryption: Option<EncryptionInfo>,
97}
98
99/// JWS file reference
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct JwsReference {
102    pub jws_file: String,
103}
104
105/// Legacy signature format (for backward compatibility)
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct LegacySignature {
108    pub alg: String,
109    pub sig: String,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub hash: Option<String>,
112}
113
114/// Encryption information in manifest
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct EncryptionInfo {
117    pub algorithm: String, // \"AES-256-GCM\"
118    pub ephemeral_public_key: String,
119    pub iv: String,
120    pub auth_tag: String,
121}
122
123/// Extensions object for custom data
124#[derive(Debug, Clone, Serialize, Deserialize, Default)]
125pub struct Extensions {
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub custom_tags: Option<Vec<String>>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub consent: Option<ConsentExtension>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub deid: Option<DeidExtension>,
132    #[serde(flatten)]
133    pub additional: HashMap<String, Value>,
134}
135
136/// Consent extension
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ConsentExtension {
139    pub status: String, // \"granted\", \"denied\", \"unknown\"
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub scope: Option<Vec<String>>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub method: Option<String>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub signed_on: Option<String>,
146}
147
148/// De-identification extension
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct DeidExtension {
151    pub keys: Vec<String>,
152}
153
154/// JMIX metadata.json structure
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct Metadata {
157    pub version: String,
158    pub id: String,
159    pub timestamp: String,
160    pub patient: Patient,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub report: Option<Report>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub studies: Option<Studies>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub extensions: Option<Extensions>,
167}
168
169/// Patient information
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct Patient {
172    pub id: String,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub name: Option<HumanName>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub dob: Option<String>, // YYYY-MM-DD
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub sex: Option<String>, // \"M\", \"F\", \"O\"
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub identifiers: Option<Vec<Identifier>>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub verification: Option<Verification>,
183}
184
185/// Human name structure
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct HumanName {
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub family: Option<String>,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub given: Option<Vec<String>>,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub prefix: Option<String>,
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub suffix: Option<String>,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub text: Option<String>,
198}
199
200/// Identifier for patients or entities
201#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct Identifier {
203    pub system: String,
204    pub value: String,
205}
206
207/// Verification information
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct Verification {
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub verified_by: Option<String>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub verified_on: Option<String>,
214}
215
216/// Report reference
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct Report {
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub file: Option<String>,
221}
222
223/// Studies information from DICOM
224#[derive(Debug, Clone, Serialize, Deserialize, Default)]
225pub struct Studies {
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub study_description: Option<String>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub study_uid: Option<String>,
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub series: Option<Vec<Series>>,
232}
233
234/// Series information from DICOM
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct Series {
237    pub series_uid: String,
238    pub modality: String,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub body_part: Option<String>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub instance_count: Option<i32>,
243}
244
245/// JMIX audit.json structure
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct Audit {
248    pub audit: Vec<AuditEntry>,
249}
250
251/// Single audit entry
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct AuditEntry {
254    pub event: String,
255    pub by: EntityRef,
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub to: Option<EntityRef>,
258    pub timestamp: String,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub assertion: Option<Assertion>,
261}
262
263/// Entity reference in audit entries
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct EntityRef {
266    pub id: String,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub name: Option<String>,
269}
270
271/// JMIX files.json structure (array of file entries)
272pub type Files = Vec<FileEntry>;
273
274/// Single file entry in files.json
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct FileEntry {
277    pub file: String,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub hash: Option<String>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub size_bytes: Option<i64>,
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_envelope_serialization_roundtrip() {
290        let envelope = Envelope {
291            manifest: Manifest {
292                version: "1.0".to_string(),
293                id: "test-id".to_string(),
294                timestamp: "2023-01-01T00:00:00Z".to_string(),
295                sender: Entity {
296                    name: Some("Test Sender".to_string()),
297                    id: "sender-1".to_string(),
298                    contact: Contact::Email("sender@example.com".to_string()),
299                    assertion: None,
300                },
301                requester: None,
302                receiver: vec![Entity {
303                    name: Some("Test Receiver".to_string()),
304                    id: "receiver-1".to_string(),
305                    contact: Contact::Email("receiver@example.com".to_string()),
306                    assertion: None,
307                }],
308                security: Security {
309                    classification: Some("confidential".to_string()),
310                    payload_hash: "sha256:abc123".to_string(),
311                    jws: None,
312                    signature: None,
313                    encryption: None,
314                },
315                extensions: None,
316            },
317            metadata: Metadata {
318                version: "1.0".to_string(),
319                id: "test-id".to_string(),
320                timestamp: "2023-01-01T00:00:00Z".to_string(),
321                patient: Patient {
322                    id: "patient-1".to_string(),
323                    name: None,
324                    dob: None,
325                    sex: None,
326                    identifiers: None,
327                    verification: None,
328                },
329                report: None,
330                studies: None,
331                extensions: None,
332            },
333            audit: Audit {
334                audit: vec![AuditEntry {
335                    event: "created".to_string(),
336                    by: EntityRef {
337                        id: "sender-1".to_string(),
338                        name: Some("Test Sender".to_string()),
339                    },
340                    to: None,
341                    timestamp: "2023-01-01T00:00:00Z".to_string(),
342                    assertion: None,
343                }],
344            },
345            manifest_jws: None,
346        };
347
348        // Test serialization to JSON
349        let json = serde_json::to_string_pretty(&envelope).expect("Failed to serialize");
350        assert!(!json.is_empty());
351
352        // Test deserialization from JSON
353        let deserialized: Envelope = serde_json::from_str(&json).expect("Failed to deserialize");
354
355        // Basic validation that key fields match
356        assert_eq!(envelope.manifest.id, deserialized.manifest.id);
357        assert_eq!(
358            envelope.metadata.patient.id,
359            deserialized.metadata.patient.id
360        );
361        assert_eq!(envelope.audit.audit.len(), deserialized.audit.audit.len());
362    }
363
364    #[test]
365    fn test_contact_variants() {
366        // Test email contact
367        let email_contact = Contact::Email("test@example.com".to_string());
368        let json = serde_json::to_string(&email_contact).expect("Failed to serialize");
369        let deserialized: Contact = serde_json::from_str(&json).expect("Failed to deserialize");
370        matches!(deserialized, Contact::Email(_));
371
372        // Test structured contact
373        let point_contact = Contact::Point(ContactPoint {
374            system: "phone".to_string(),
375            value: "+1234567890".to_string(),
376        });
377        let json = serde_json::to_string(&point_contact).expect("Failed to serialize");
378        let deserialized: Contact = serde_json::from_str(&json).expect("Failed to deserialize");
379        matches!(deserialized, Contact::Point(_));
380    }
381}