oca_presentation/presentation/
mod.rs

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        // Serialize the Range as an array of two elements
111        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}