1use baseid_core::error::SerializationError;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct MobileDocument {
9 pub doc_type: String,
11 pub namespaces: Namespaces,
13}
14
15pub type Namespaces = std::collections::BTreeMap<String, Vec<DataElement>>;
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub struct DataElement {
21 pub identifier: String,
23 pub value: serde_json::Value,
25}
26
27impl MobileDocument {
28 pub fn to_cbor(&self) -> baseid_core::Result<Vec<u8>> {
30 let mut buf = Vec::new();
31 ciborium::into_writer(self, &mut buf).map_err(|_| SerializationError::Cbor)?;
32 Ok(buf)
33 }
34
35 pub fn from_cbor(bytes: &[u8]) -> baseid_core::Result<Self> {
37 ciborium::from_reader(bytes).map_err(|_| SerializationError::Cbor.into())
38 }
39}
40
41impl DataElement {
42 pub fn to_cbor(&self) -> baseid_core::Result<Vec<u8>> {
47 let mut buf = Vec::new();
48 ciborium::into_writer(self, &mut buf).map_err(|_| SerializationError::Cbor)?;
49 Ok(buf)
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use std::collections::BTreeMap;
57
58 fn sample_doc() -> MobileDocument {
59 let mut namespaces = BTreeMap::new();
60 namespaces.insert(
61 "org.iso.18013.5.1".to_string(),
62 vec![
63 DataElement {
64 identifier: "family_name".to_string(),
65 value: serde_json::json!("Doe"),
66 },
67 DataElement {
68 identifier: "given_name".to_string(),
69 value: serde_json::json!("John"),
70 },
71 DataElement {
72 identifier: "document_number".to_string(),
73 value: serde_json::json!("DL-123456"),
74 },
75 ],
76 );
77 MobileDocument {
78 doc_type: "org.iso.18013.5.1.mDL".to_string(),
79 namespaces,
80 }
81 }
82
83 #[test]
84 fn cbor_roundtrip() {
85 let doc = sample_doc();
86 let cbor = doc.to_cbor().unwrap();
87 let restored = MobileDocument::from_cbor(&cbor).unwrap();
88 assert_eq!(restored.doc_type, doc.doc_type);
89 assert_eq!(restored.namespaces.len(), 1);
90
91 let ns = &restored.namespaces["org.iso.18013.5.1"];
92 assert_eq!(ns.len(), 3);
93 assert_eq!(ns[0].identifier, "family_name");
94 assert_eq!(ns[0].value, serde_json::json!("Doe"));
95 }
96
97 #[test]
98 fn data_element_cbor() {
99 let elem = DataElement {
100 identifier: "family_name".to_string(),
101 value: serde_json::json!("Smith"),
102 };
103 let cbor = elem.to_cbor().unwrap();
104 assert!(!cbor.is_empty());
105
106 let cbor2 = elem.to_cbor().unwrap();
108 assert_eq!(cbor, cbor2);
109 }
110
111 #[test]
114 fn cbor_parseable_by_raw_ciborium() {
115 let doc = sample_doc();
116 let cbor = doc.to_cbor().unwrap();
117
118 let raw: ciborium::Value = ciborium::from_reader(&cbor[..]).unwrap();
120
121 let map = raw.as_map().expect("top-level should be a CBOR map");
123 assert!(map.len() >= 2, "should have doc_type and namespaces");
124
125 let doc_type_entry = map.iter().find(|(k, _)| k.as_text() == Some("doc_type"));
127 assert!(doc_type_entry.is_some(), "should have doc_type key");
128 let (_, doc_type_val) = doc_type_entry.unwrap();
129 assert_eq!(doc_type_val.as_text(), Some("org.iso.18013.5.1.mDL"));
130 }
131
132 #[test]
133 fn data_element_cbor_parseable_by_raw_ciborium() {
134 let elem = DataElement {
135 identifier: "given_name".to_string(),
136 value: serde_json::json!("Jane"),
137 };
138 let cbor = elem.to_cbor().unwrap();
139
140 let raw: ciborium::Value = ciborium::from_reader(&cbor[..]).unwrap();
142 let map = raw.as_map().expect("element should be a CBOR map");
143
144 let id_entry = map.iter().find(|(k, _)| k.as_text() == Some("identifier"));
145 assert!(id_entry.is_some());
146 assert_eq!(id_entry.unwrap().1.as_text(), Some("given_name"));
147
148 let val_entry = map.iter().find(|(k, _)| k.as_text() == Some("value"));
149 assert!(val_entry.is_some());
150 assert_eq!(val_entry.unwrap().1.as_text(), Some("Jane"));
151 }
152
153 #[test]
154 fn cbor_roundtrip_preserves_complex_values() {
155 let mut namespaces = BTreeMap::new();
156 namespaces.insert(
157 "org.iso.18013.5.1".to_string(),
158 vec![
159 DataElement {
160 identifier: "age_over_18".to_string(),
161 value: serde_json::json!(true),
162 },
163 DataElement {
164 identifier: "age_in_years".to_string(),
165 value: serde_json::json!(25),
166 },
167 DataElement {
168 identifier: "portrait".to_string(),
169 value: serde_json::json!(null),
170 },
171 ],
172 );
173 let doc = MobileDocument {
174 doc_type: "org.iso.18013.5.1.mDL".to_string(),
175 namespaces,
176 };
177
178 let cbor = doc.to_cbor().unwrap();
179 let restored = MobileDocument::from_cbor(&cbor).unwrap();
180
181 assert_eq!(
182 restored.namespaces["org.iso.18013.5.1"][0].value,
183 serde_json::json!(true)
184 );
185 assert_eq!(
186 restored.namespaces["org.iso.18013.5.1"][1].value,
187 serde_json::json!(25)
188 );
189 assert_eq!(
190 restored.namespaces["org.iso.18013.5.1"][2].value,
191 serde_json::json!(null)
192 );
193 }
194}