1use std::collections::BTreeMap;
2
3use isolang::Language;
4use said::sad::{SerializationFormats, SAD};
5use said::derivation::HashFunctionCode;
6use serde::{Deserialize, Serialize, Serializer};
7use serialization::opt_serialization;
8
9use crate::page::Page;
10use indexmap::IndexMap;
11mod serialization;
12
13#[derive(Debug, thiserror::Error)]
14pub enum PresentationError {
15 #[error("Said doesn't match presentation")]
16 SaidDoesNotMatch,
17 #[error("`d` field is empty")]
18 MissingSaid,
19}
20
21#[derive(Debug, SAD, Deserialize)]
22pub struct Presentation {
23 #[serde(rename = "v")]
24 pub version: String,
25 #[serde(rename = "bd")]
26 pub bundle_digest: said::SelfAddressingIdentifier,
27 #[serde(rename = "l")]
28 pub languages: Vec<Language>,
29 #[said]
30 #[serde(rename = "d")]
31 #[serde(deserialize_with = "opt_serialization::empty_str_as_none")]
32 pub said: Option<said::SelfAddressingIdentifier>,
33 #[serde(rename = "p")]
34 pub pages: Vec<Page>,
35 #[serde(rename = "po")]
36 pub pages_order: Vec<String>,
37 #[serde(rename = "pl")]
38 pub pages_label: IndexMap<Language, BTreeMap<String, String>>,
39 #[serde(rename = "i")]
40 pub interaction: Vec<Interaction>,
41}
42
43impl Presentation {
44 pub fn validate_digest(&self) -> Result<(), PresentationError> {
45 let code = HashFunctionCode::Blake3_256;
46 let format = SerializationFormats::JSON;
47 let der_data = self.derivation_data(&code, &format);
48 if self
49 .said
50 .as_ref()
51 .ok_or(PresentationError::MissingSaid)?
52 .verify_binding(&der_data)
53 {
54 Ok(())
55 } else {
56 Err(PresentationError::SaidDoesNotMatch)
57 }
58 }
59}
60
61#[derive(Debug, Serialize, Deserialize, Clone)]
62pub struct Interaction {
63 #[serde(rename = "m")]
64 pub interaction_method: InteractionMethod,
65 #[serde(rename = "c")]
66 pub context: Context,
67 #[serde(rename = "a")]
68 pub attr_properties: IndexMap<String, AttrType>,
69}
70
71#[derive(Debug, Serialize, Deserialize, Clone)]
72#[serde(rename_all = "lowercase")]
73#[serde(tag = "t")]
74pub enum AttrType {
75 TextArea,
76 Signature {
77 #[serde(skip_serializing_if = "Option::is_none")]
78 m: Option<SignatureMetadata>,
79 },
80 File,
81 Radio {
82 o: Orientation,
83 },
84 Time,
85 DateTime,
86 Date,
87 #[serde(rename = "code_scanner")]
88 CodeScanner,
89 Select {
90 va: Cardinality,
91 },
92 Number {
93 r: Range,
94 s: f32,
95 },
96 Question {
97 answer: String,
98 o: IndexMap<String, Vec<String>>,
99 },
100}
101
102#[derive(Debug, Deserialize, Clone)]
103pub struct Range([Option<f32>; 2]);
104use serde::ser::SerializeSeq;
105impl Serialize for Range {
106 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
107 where
108 S: Serializer,
109 {
110 let mut seq = serializer.serialize_seq(Some(2))?;
112 let first = &self.0[0];
113 if first.map(|i| i.fract()) == Some(0.0) {
114 seq.serialize_element(&(first.map(|i| i as i32)))?;
115 } else {
116 seq.serialize_element(&first)?;
117 }
118 let second = &self.0[1];
119 if second.map(|i| i.fract()) == Some(0.0) {
120 seq.serialize_element(&(second.map(|i| i as i32)))?;
121 } else {
122 seq.serialize_element(&second)?;
123 }
124
125 seq.end()
126 }
127}
128
129#[derive(Debug, Serialize, Deserialize, Clone)]
130pub struct SignatureMetadata {
131 canvas: String,
132 geolocation: Geolocation,
133}
134
135#[derive(Debug, Serialize, Deserialize, Clone)]
136pub struct Geolocation {
137 latitude: String,
138 longitude: String,
139 accuracy: String,
140 timestamp: String,
141}
142
143#[derive(Debug, Serialize, Deserialize, Clone)]
144#[serde(rename_all = "lowercase")]
145pub enum Orientation {
146 Horizontal,
147 Vertical,
148}
149
150#[derive(Debug, Serialize, Deserialize, Clone)]
151#[serde(rename_all = "lowercase")]
152pub enum Cardinality {
153 Multiple,
154}
155
156#[derive(Debug, Serialize, Deserialize, Clone)]
157#[serde(rename_all = "lowercase")]
158pub enum Context {
159 Capture,
160}
161#[derive(Debug, Serialize, Deserialize, Clone)]
162#[serde(rename_all = "lowercase")]
163pub enum InteractionMethod {
164 Web,
165 Ai,
166}
167
168#[cfg(test)]
169mod tests {
170 use crate::page::PageElement;
171
172 use super::*;
173
174 #[test]
175 fn test_presentation_base() {
176 let page_y = Page {
177 name: "pageY".to_string(),
178 attribute_order: vec![PageElement::Value("attr_1".to_string())],
179 };
180 let page_z = Page {
181 name: "pageZ".to_string(),
182 attribute_order: vec![
183 PageElement::Value("attr_3".to_string()),
184 PageElement::Value("attr_2".to_string()),
185 ],
186 };
187 let pages = vec![page_y, page_z];
188
189 let mut pages_label = IndexMap::new();
190 let mut pages_label_en = BTreeMap::new();
191 pages_label_en.insert("pageY".to_string(), "Page Y".to_string());
192 pages_label_en.insert("pageZ".to_string(), "Page Z".to_string());
193 pages_label.insert(Language::Eng, pages_label_en);
194
195 let mut presentation_base = Presentation {
196 version: "1.0.0".to_string(),
197 bundle_digest: "EHp19U2U1sdOBmPzMmILM3DUI0PQph9tdN3KtmBrvNV7"
198 .parse()
199 .unwrap(),
200 languages: vec![Language::Eng, Language::Pol, Language::Deu],
201 said: None,
202 pages,
203 pages_order: vec!["pageY".to_string(), "pageZ".to_string()],
204 pages_label,
205 interaction: vec![Interaction {
206 interaction_method: InteractionMethod::Web,
207 context: Context::Capture,
208 attr_properties: vec![(
209 "attr_1".to_string(),
210 AttrType::Radio {
211 o: Orientation::Horizontal,
212 },
213 )]
214 .into_iter()
215 .collect(),
216 }],
217 };
218
219 presentation_base.compute_digest();
220
221 println!(
222 "{}",
223 serde_json::to_string_pretty(&presentation_base).unwrap()
224 );
225 let der_data = presentation_base.derivation_data();
226 let sai = presentation_base.said.unwrap();
227 assert!(sai.verify_binding(&der_data));
228 assert_eq!(
229 sai.to_string(),
230 "EOiPlSDMJlllCZHT4skyPLlpy0tOXsOJNxP2ifhexL4b".to_string()
231 );
232 }
233
234 #[test]
235 fn test_deserialize() {
236 let input = r#"{
237 "v":"1.0.0",
238 "bd": "EHp19U2U1sdOBmPzMmILM3DUI0PQph9tdN3KtmBrvNV7",
239 "l": ["eng", "pol", "deu"],
240 "d": "",
241 "p": [
242 {
243 "n": "pageY",
244 "ao": [
245 "attr_1"
246 ]
247 },
248 {
249 "n": "pageZ",
250 "ao": [
251 "attr_3",
252 "attr_2"
253 ]
254 }
255 ],
256 "po": [
257 "pageY",
258 "pageZ"
259 ],
260 "pl": {
261 "eng": {
262 "pageY": "Page Y",
263 "pageZ": "Page Z"
264 }
265 },
266 "i": [
267 {
268 "m": "web",
269 "c": "capture",
270 "a": {
271 "attr_1": {
272 "t": "textarea"
273 }
274 }
275 }
276 ]
277}"#;
278
279 let pres: Presentation = serde_json::from_str(input).unwrap();
280 assert!(pres.said.is_none());
281
282 let mut serialized = serde_json::to_string_pretty(&pres).unwrap();
283 serialized.retain(|c| !c.is_whitespace());
284 let mut expected = input.to_string();
285 expected.retain(|c| !c.is_whitespace());
286
287 assert_eq!(serialized, expected);
288 }
289
290 #[test]
291 fn test_complex_deserialize() {
292 let input = r#"{
293 "v": "1.0.0",
294 "bd": "EIRYpj7kwFW1nJ9AInPgMjsdC-DeX26eHlb7FzwzlkEh",
295 "l": [
296 "eng",
297 "pol",
298 "deu"
299 ],
300 "d": "",
301 "p": [
302 {
303 "n": "page 2",
304 "ao": [
305 "select",
306 "i",
307 "img",
308 "num",
309 "date",
310 "time",
311 "nice_attr"
312 ]
313 },
314 {
315 "n": "page 1",
316 "ao": [
317 "passed",
318 "d",
319 "sign",
320 {
321 "n": "customer",
322 "ao": [
323 "name",
324 "surname",
325 {
326 "n": "building",
327 "ao": [
328 "floors",
329 "area",
330 {
331 "n": "address",
332 "ao": [
333 "city",
334 "zip",
335 "street"
336 ]
337 }
338 ]
339 }
340 ]
341 }
342 ]
343 },
344 {
345 "n": "page 3",
346 "ao": [
347 "list_text",
348 "list_num",
349 "list_bool",
350 "list_date",
351 {
352 "n": "devices",
353 "ao": [
354 "name",
355 "description",
356 {
357 "n": "manufacturer",
358 "ao": [
359 "name",
360 {
361 "n": "address",
362 "ao": ["city", "zip"]
363 },
364 {
365 "n": "parts",
366 "ao": ["name"]
367 }
368 ]
369 }
370 ]
371 }
372 ]
373 },
374 {
375 "n": "page 4",
376 "ao": [
377 "text_attr1",
378 "radio1",
379 "text_attr2",
380 "radio2"
381 ]
382 }
383 ],
384 "po": [
385 "page 1",
386 "page 2",
387 "page 3",
388 "page 4"
389 ],
390 "pl": {
391 "eng": {
392 "page 1": "First page",
393 "page 2": "Second page",
394 "page 3": "Third page",
395 "page 4": "Radio/checkbox page"
396 },
397 "pol": {
398 "page 1": "Pierwsza strona",
399 "page 2": "Druga strona",
400 "page 3": "Trzecia strona",
401 "page 4": "Radio/checkbox strona"
402 },
403 "deu": {
404 "page 1": "Erste Seite",
405 "page 2": "Zweite Seite",
406 "page 3": "Dritte Seite",
407 "page 4": "Radio/checkbox Seite"
408 }
409 },
410 "i": [
411 {
412 "m": "web",
413 "c": "capture",
414 "a": {
415 "d": {
416 "t": "textarea"
417 },
418 "img": {
419 "t": "file"
420 },
421 "sign": {
422 "t": "signature"
423 },
424 "radio1": {
425 "t": "radio",
426 "o": "vertical"
427 },
428 "radio2": {
429 "t": "radio",
430 "o": "horizontal"
431 },
432 "date": {
433 "t": "date"
434 },
435 "time": {
436 "t": "time"
437 },
438 "list_date": {
439 "t": "datetime"
440 },
441 "customer.building.address.street": {
442 "t": "textarea"
443 },
444 "question1": {
445 "t": "question",
446 "answer": "r",
447 "o": { "no": ["on_no_what", "on_no_when"], "maybe": ["on_maybe"] }
448 }
449 }
450 }
451 ]
452}
453"#;
454
455 let pres: Presentation = serde_json::from_str(input).unwrap();
456 assert!(pres.said.is_none());
457
458 let mut serialized = serde_json::to_string_pretty(&pres).unwrap();
459 serialized.retain(|c| !c.is_whitespace());
460 let mut expected = input.to_string();
461 expected.retain(|c| !c.is_whitespace());
462
463 assert_eq!(serialized, expected);
464 }
465
466 #[test]
467 fn test_attribute() {
468 let attr_str = r#"{
469 "t": "question",
470 "answer": "r",
471 "o": { "no": ["on_no_what", "on_no_when"], "maybe": ["on_maybe"] }
472 }"#;
473 let attr: AttrType = serde_json::from_str(&attr_str).unwrap();
474 assert!(matches!(attr, AttrType::Question { answer: _, o: _ }))
475 }
476}