Skip to main content

prelude_xml_parser/native/
common.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[cfg(feature = "python")]
5use pyo3::{
6    prelude::*,
7    types::{PyDateTime, PyDict},
8};
9
10use crate::native::deserializers::{
11    default_datetime_none, default_string_none, deserialize_empty_string_as_none,
12    deserialize_empty_string_as_none_datetime,
13};
14
15#[cfg(feature = "python")]
16use crate::native::deserializers::{to_py_datetime, to_py_datetime_option};
17
18#[cfg(not(feature = "python"))]
19#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
20pub struct Value {
21    #[serde(rename = "by")]
22    #[serde(alias = "@by")]
23    #[serde(alias = "by")]
24    pub by: String,
25
26    #[serde(rename = "byUniqueId")]
27    #[serde(alias = "@byUniqueId")]
28    #[serde(alias = "byUniqueId")]
29    #[serde(
30        default = "default_string_none",
31        deserialize_with = "deserialize_empty_string_as_none"
32    )]
33    pub by_unique_id: Option<String>,
34    #[serde(rename = "role")]
35    #[serde(alias = "@role")]
36    #[serde(alias = "role")]
37    pub role: String,
38    #[serde(rename = "when")]
39    #[serde(alias = "@when")]
40    #[serde(alias = "when")]
41    pub when: Option<DateTime<Utc>>,
42
43    #[serde(rename = "value")]
44    #[serde(alias = "$text")]
45    #[serde(alias = "#text")]
46    #[serde(alias = "value")]
47    #[serde(default)]
48    pub value: String,
49}
50
51#[cfg(feature = "python")]
52#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
53#[pyclass(skip_from_py_object)]
54pub struct Value {
55    #[serde(rename = "by")]
56    #[serde(alias = "@by")]
57    #[serde(alias = "by")]
58    pub by: String,
59
60    #[serde(rename = "byUniqueId")]
61    #[serde(alias = "@byUniqueId")]
62    #[serde(alias = "byUniqueId")]
63    #[serde(
64        default = "default_string_none",
65        deserialize_with = "deserialize_empty_string_as_none"
66    )]
67    pub by_unique_id: Option<String>,
68    #[serde(rename = "role")]
69    #[serde(alias = "@role")]
70    #[serde(alias = "role")]
71    pub role: String,
72    #[serde(rename = "when")]
73    #[serde(alias = "@when")]
74    #[serde(alias = "when")]
75    pub when: Option<DateTime<Utc>>,
76
77    #[serde(rename = "value")]
78    #[serde(alias = "$text")]
79    #[serde(alias = "#text")]
80    #[serde(alias = "value")]
81    #[serde(default)]
82    pub value: String,
83}
84
85#[cfg(feature = "python")]
86#[pymethods]
87impl Value {
88    #[getter]
89    fn by(&self) -> PyResult<String> {
90        Ok(self.by.clone())
91    }
92
93    #[getter]
94    fn by_unique_id(&self) -> PyResult<Option<String>> {
95        Ok(self.by_unique_id.clone())
96    }
97
98    #[getter]
99    fn role(&self) -> PyResult<String> {
100        Ok(self.role.clone())
101    }
102
103    #[getter]
104    fn when<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
105        to_py_datetime_option(py, &self.when)
106    }
107
108    #[getter]
109    fn value(&self) -> PyResult<String> {
110        Ok(self.value.clone())
111    }
112
113    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
114        let dict = PyDict::new(py);
115        dict.set_item("by", &self.by)?;
116        dict.set_item("by_unique_id", &self.by_unique_id)?;
117        dict.set_item("role", &self.role)?;
118        dict.set_item("when", to_py_datetime_option(py, &self.when)?)?;
119        dict.set_item("value", &self.value)?;
120
121        Ok(dict)
122    }
123}
124
125#[cfg(not(feature = "python"))]
126#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
127pub struct Reason {
128    #[serde(rename = "by")]
129    #[serde(alias = "@by")]
130    #[serde(alias = "by")]
131    pub by: String,
132
133    #[serde(rename = "byUniqueId")]
134    #[serde(alias = "@byUniqueId")]
135    #[serde(alias = "byUniqueId")]
136    #[serde(
137        default = "default_string_none",
138        deserialize_with = "deserialize_empty_string_as_none"
139    )]
140    pub by_unique_id: Option<String>,
141
142    #[serde(rename = "role")]
143    #[serde(alias = "@role")]
144    #[serde(alias = "role")]
145    pub role: String,
146    #[serde(rename = "when")]
147    #[serde(alias = "@when")]
148    #[serde(alias = "when")]
149    pub when: Option<DateTime<Utc>>,
150
151    #[serde(rename = "value")]
152    #[serde(alias = "$text")]
153    #[serde(alias = "#text")]
154    #[serde(alias = "value")]
155    #[serde(default)]
156    pub value: String,
157}
158
159#[cfg(feature = "python")]
160#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
161#[pyclass(skip_from_py_object)]
162pub struct Reason {
163    #[serde(rename = "by")]
164    #[serde(alias = "@by")]
165    #[serde(alias = "by")]
166    pub by: String,
167
168    #[serde(rename = "byUniqueId")]
169    #[serde(alias = "@byUniqueId")]
170    #[serde(alias = "byUniqueId")]
171    #[serde(
172        default = "default_string_none",
173        deserialize_with = "deserialize_empty_string_as_none"
174    )]
175    pub by_unique_id: Option<String>,
176
177    #[serde(rename = "role")]
178    #[serde(alias = "@role")]
179    #[serde(alias = "role")]
180    pub role: String,
181    #[serde(rename = "when")]
182    #[serde(alias = "@when")]
183    #[serde(alias = "when")]
184    pub when: Option<DateTime<Utc>>,
185
186    #[serde(rename = "value")]
187    #[serde(alias = "$text")]
188    #[serde(alias = "#text")]
189    #[serde(alias = "value")]
190    #[serde(default)]
191    pub value: String,
192}
193
194#[cfg(feature = "python")]
195#[pymethods]
196impl Reason {
197    #[getter]
198    fn by(&self) -> PyResult<String> {
199        Ok(self.by.clone())
200    }
201
202    #[getter]
203    fn by_unique_id(&self) -> PyResult<Option<String>> {
204        Ok(self.by_unique_id.clone())
205    }
206
207    #[getter]
208    fn role(&self) -> PyResult<String> {
209        Ok(self.role.clone())
210    }
211
212    #[getter]
213    fn when<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
214        to_py_datetime_option(py, &self.when)
215    }
216
217    #[getter]
218    fn value(&self) -> PyResult<String> {
219        Ok(self.value.clone())
220    }
221
222    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
223        let dict = PyDict::new(py);
224        dict.set_item("by", &self.by)?;
225        dict.set_item("by_unique_id", &self.by_unique_id)?;
226        dict.set_item("role", &self.role)?;
227        dict.set_item("when", to_py_datetime_option(py, &self.when)?)?;
228        dict.set_item("value", &self.value)?;
229
230        Ok(dict)
231    }
232}
233
234#[cfg(not(feature = "python"))]
235#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
236pub struct Entry {
237    #[serde(rename = "entryId")]
238    #[serde(alias = "@id")]
239    #[serde(alias = "entryId")]
240    pub entry_id: String,
241
242    #[serde(rename = "reviewedBy")]
243    #[serde(alias = "@reviewedBy")]
244    #[serde(alias = "reviewedBy")]
245    #[serde(
246        default = "default_string_none",
247        deserialize_with = "deserialize_empty_string_as_none"
248    )]
249    pub reviewed_by: Option<String>,
250
251    #[serde(rename = "reviewedByUniqueId")]
252    #[serde(alias = "@reviewedByUniqueId")]
253    #[serde(alias = "reviewedByUniqueId")]
254    #[serde(
255        default = "default_string_none",
256        deserialize_with = "deserialize_empty_string_as_none"
257    )]
258    pub reviewed_by_unique_id: Option<String>,
259
260    #[serde(rename = "reviewedByWhen")]
261    #[serde(alias = "@reviewedByWhen")]
262    #[serde(alias = "reviewedByWhen")]
263    #[serde(
264        default = "default_datetime_none",
265        deserialize_with = "deserialize_empty_string_as_none_datetime"
266    )]
267    pub reviewed_by_when: Option<DateTime<Utc>>,
268
269    pub value: Option<Value>,
270    pub reason: Option<Reason>,
271}
272
273#[cfg(feature = "python")]
274#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
275#[pyclass(skip_from_py_object)]
276pub struct Entry {
277    #[serde(rename = "entryId")]
278    #[serde(alias = "@id")]
279    #[serde(alias = "entryId")]
280    pub entry_id: String,
281
282    #[serde(rename = "reviewedBy")]
283    #[serde(alias = "@reviewedBy")]
284    #[serde(alias = "reviewedBy")]
285    #[serde(
286        default = "default_string_none",
287        deserialize_with = "deserialize_empty_string_as_none"
288    )]
289    pub reviewed_by: Option<String>,
290
291    #[serde(rename = "reviewedByUniqueId")]
292    #[serde(alias = "@reviewedByUniqueId")]
293    #[serde(alias = "reviewedByUniqueId")]
294    #[serde(
295        default = "default_string_none",
296        deserialize_with = "deserialize_empty_string_as_none"
297    )]
298    pub reviewed_by_unique_id: Option<String>,
299
300    #[serde(rename = "reviewedByWhen")]
301    #[serde(alias = "@reviewedByWhen")]
302    #[serde(alias = "reviewedByWhen")]
303    #[serde(
304        default = "default_datetime_none",
305        deserialize_with = "deserialize_empty_string_as_none_datetime"
306    )]
307    pub reviewed_by_when: Option<DateTime<Utc>>,
308
309    pub value: Option<Value>,
310    pub reason: Option<Reason>,
311}
312
313#[cfg(feature = "python")]
314#[pymethods]
315impl Entry {
316    #[getter]
317    fn entry_id(&self) -> PyResult<String> {
318        Ok(self.entry_id.clone())
319    }
320
321    #[getter]
322    fn reviewed_by(&self) -> PyResult<Option<String>> {
323        Ok(self.reviewed_by.clone())
324    }
325
326    #[getter]
327    fn reviewed_by_unique_id(&self) -> PyResult<Option<String>> {
328        Ok(self.reviewed_by_unique_id.clone())
329    }
330
331    #[getter]
332    fn reviewed_by_when<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
333        to_py_datetime_option(py, &self.reviewed_by_when)
334    }
335
336    #[getter]
337    fn value(&self) -> PyResult<Option<Value>> {
338        Ok(self.value.clone())
339    }
340
341    #[getter]
342    fn reason(&self) -> PyResult<Option<Reason>> {
343        Ok(self.reason.clone())
344    }
345
346    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
347        let dict = PyDict::new(py);
348        dict.set_item("entry_id", &self.entry_id)?;
349        dict.set_item("reviewed_by", &self.reviewed_by)?;
350        dict.set_item("reviewed_by_unique_id", &self.reviewed_by_unique_id)?;
351        dict.set_item(
352            "reviewed_by_when",
353            to_py_datetime_option(py, &self.reviewed_by_when)?,
354        )?;
355        if let Some(value) = &self.value {
356            dict.set_item("value", value.to_dict(py)?)?;
357        } else {
358            dict.set_item("value", py.None())?;
359        }
360        if let Some(reason) = &self.reason {
361            dict.set_item("reason", reason.to_dict(py)?)?;
362        } else {
363            dict.set_item("reason", py.None())?;
364        }
365
366        Ok(dict)
367    }
368}
369
370#[cfg(not(feature = "python"))]
371#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
372pub struct Comment {
373    #[serde(rename = "commentId")]
374    #[serde(alias = "@id")]
375    #[serde(alias = "commentId")]
376    pub comment_id: String,
377    pub value: Option<Value>,
378}
379
380#[cfg(feature = "python")]
381#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
382#[pyclass(get_all, skip_from_py_object)]
383pub struct Comment {
384    #[serde(rename = "commentId")]
385    #[serde(alias = "@id")]
386    #[serde(alias = "commentId")]
387    pub comment_id: String,
388    pub value: Option<Value>,
389}
390
391#[cfg(feature = "python")]
392#[pymethods]
393impl Comment {
394    #[getter]
395    fn comment_id(&self) -> PyResult<String> {
396        Ok(self.comment_id.clone())
397    }
398
399    #[getter]
400    fn value(&self) -> PyResult<Option<Value>> {
401        Ok(self.value.clone())
402    }
403
404    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
405        let dict = PyDict::new(py);
406        dict.set_item("comment_id", &self.comment_id)?;
407        if let Some(value) = &self.value {
408            dict.set_item("value", value.to_dict(py)?)?;
409        } else {
410            dict.set_item("value", py.None())?;
411        }
412
413        Ok(dict)
414    }
415}
416
417#[cfg(not(feature = "python"))]
418#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
419pub struct Field {
420    #[serde(rename = "name")]
421    #[serde(alias = "@name")]
422    #[serde(alias = "name")]
423    pub name: String,
424
425    #[serde(rename = "fieldType")]
426    #[serde(alias = "@type")]
427    #[serde(alias = "fieldType")]
428    pub field_type: String,
429
430    #[serde(rename = "dataType")]
431    #[serde(alias = "@dataType")]
432    #[serde(alias = "dataType")]
433    #[serde(
434        default = "default_string_none",
435        deserialize_with = "deserialize_empty_string_as_none"
436    )]
437    pub data_type: Option<String>,
438    #[serde(rename = "errorCode")]
439    #[serde(alias = "@errorCode")]
440    #[serde(alias = "errorCode")]
441    pub error_code: String,
442    #[serde(rename = "whenCreated")]
443    #[serde(alias = "@whenCreated")]
444    #[serde(alias = "whenCreated")]
445    pub when_created: Option<DateTime<Utc>>,
446    #[serde(rename = "keepHistory")]
447    #[serde(alias = "@keepHistory")]
448    #[serde(alias = "keepHistory")]
449    pub keep_history: bool,
450
451    #[serde(alias = "entry")]
452    pub entries: Option<Vec<Entry>>,
453
454    #[serde(alias = "comment")]
455    pub comments: Option<Vec<Comment>>,
456}
457
458#[cfg(feature = "python")]
459#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
460#[pyclass(skip_from_py_object)]
461pub struct Field {
462    #[serde(rename = "name")]
463    #[serde(alias = "@name")]
464    #[serde(alias = "name")]
465    pub name: String,
466
467    #[serde(rename = "fieldType")]
468    #[serde(alias = "@type")]
469    #[serde(alias = "fieldType")]
470    pub field_type: String,
471
472    #[serde(rename = "dataType")]
473    #[serde(alias = "@dataType")]
474    #[serde(alias = "dataType")]
475    #[serde(
476        default = "default_string_none",
477        deserialize_with = "deserialize_empty_string_as_none"
478    )]
479    pub data_type: Option<String>,
480
481    #[serde(rename = "errorCode")]
482    #[serde(alias = "@errorCode")]
483    #[serde(alias = "errorCode")]
484    pub error_code: String,
485    #[serde(rename = "whenCreated")]
486    #[serde(alias = "@whenCreated")]
487    #[serde(alias = "whenCreated")]
488    pub when_created: Option<DateTime<Utc>>,
489    #[serde(rename = "keepHistory")]
490    #[serde(alias = "@keepHistory")]
491    #[serde(alias = "keepHistory")]
492    pub keep_history: bool,
493
494    #[serde(alias = "entry")]
495    pub entries: Option<Vec<Entry>>,
496
497    #[serde(alias = "comment")]
498    pub comments: Option<Vec<Comment>>,
499}
500
501#[cfg(feature = "python")]
502#[pymethods]
503impl Field {
504    #[getter]
505    fn name(&self) -> PyResult<String> {
506        Ok(self.name.clone())
507    }
508
509    #[getter]
510    fn field_type(&self) -> PyResult<String> {
511        Ok(self.field_type.clone())
512    }
513
514    #[getter]
515    fn data_type(&self) -> PyResult<Option<String>> {
516        Ok(self.data_type.clone())
517    }
518
519    #[getter]
520    fn error_code(&self) -> PyResult<String> {
521        Ok(self.error_code.clone())
522    }
523
524    #[getter]
525    fn when_created<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
526        self.when_created
527            .as_ref()
528            .map(|dt| to_py_datetime(py, dt))
529            .transpose()
530    }
531
532    #[getter]
533    fn keep_history(&self) -> PyResult<bool> {
534        Ok(self.keep_history)
535    }
536
537    #[getter]
538    fn entries(&self) -> PyResult<Option<Vec<Entry>>> {
539        Ok(self.entries.clone())
540    }
541
542    #[getter]
543    fn comments(&self) -> PyResult<Option<Vec<Comment>>> {
544        Ok(self.comments.clone())
545    }
546
547    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
548        let dict = PyDict::new(py);
549        dict.set_item("name", &self.name)?;
550        dict.set_item("field_type", &self.field_type)?;
551        dict.set_item("data_type", &self.data_type)?;
552        dict.set_item("error_code", &self.error_code)?;
553        dict.set_item(
554            "when_created",
555            self.when_created
556                .as_ref()
557                .map(|dt| to_py_datetime(py, dt))
558                .transpose()?,
559        )?;
560        dict.set_item("keep_history", self.keep_history)?;
561
562        let mut entry_dicts = Vec::new();
563        if let Some(entries) = &self.entries {
564            for entry in entries {
565                let entry_dict = entry.to_dict(py)?;
566                entry_dicts.push(entry_dict);
567            }
568            dict.set_item("entries", entry_dicts)?;
569        } else {
570            dict.set_item("entries", py.None())?;
571        }
572
573        let mut comment_dicts = Vec::new();
574        if let Some(comments) = &self.comments {
575            for comment in comments {
576                let comment_dict = comment.to_dict(py)?;
577                comment_dicts.push(comment_dict);
578            }
579            dict.set_item("comments", comment_dicts)?;
580        } else {
581            dict.set_item("comments", py.None())?;
582        }
583
584        Ok(dict)
585    }
586}
587
588#[cfg(not(feature = "python"))]
589#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
590pub struct Category {
591    #[serde(rename = "name")]
592    #[serde(alias = "@name")]
593    #[serde(alias = "name")]
594    pub name: String,
595
596    #[serde(rename = "categoryType")]
597    #[serde(alias = "@type")]
598    #[serde(alias = "categoryType")]
599    pub category_type: String,
600
601    #[serde(rename = "highestIndex")]
602    #[serde(alias = "@highestIndex")]
603    #[serde(alias = "highestIndex")]
604    pub highest_index: usize,
605
606    #[serde(alias = "field")]
607    pub fields: Option<Vec<Field>>,
608}
609
610#[cfg(feature = "python")]
611#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
612#[pyclass(get_all, skip_from_py_object)]
613pub struct Category {
614    #[serde(rename = "name")]
615    #[serde(alias = "@name")]
616    #[serde(alias = "name")]
617    pub name: String,
618
619    #[serde(rename = "categoryType")]
620    #[serde(alias = "@type")]
621    #[serde(alias = "categoryType")]
622    pub category_type: String,
623
624    #[serde(rename = "highestIndex")]
625    #[serde(alias = "@highestIndex")]
626    #[serde(alias = "highestIndex")]
627    pub highest_index: usize,
628
629    #[serde(alias = "field")]
630    pub fields: Option<Vec<Field>>,
631}
632
633#[cfg(feature = "python")]
634#[pymethods]
635impl Category {
636    #[getter]
637    fn name(&self) -> PyResult<String> {
638        Ok(self.name.clone())
639    }
640
641    #[getter]
642    fn category_type(&self) -> PyResult<String> {
643        Ok(self.category_type.clone())
644    }
645
646    #[getter]
647    fn highest_index(&self) -> PyResult<usize> {
648        Ok(self.highest_index)
649    }
650
651    #[getter]
652    fn fields(&self) -> PyResult<Option<Vec<Field>>> {
653        Ok(self.fields.clone())
654    }
655
656    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
657        let dict = PyDict::new(py);
658        dict.set_item("name", &self.name)?;
659        dict.set_item("category_type", &self.category_type)?;
660        dict.set_item("highest_index", self.highest_index)?;
661
662        let mut field_dicts = Vec::new();
663        if let Some(fields) = &self.fields {
664            for field in fields {
665                let field_dict = field.to_dict(py)?;
666                field_dicts.push(field_dict);
667            }
668            dict.set_item("fields", field_dicts)?;
669        } else {
670            dict.set_item("fields", py.None())?;
671        }
672
673        Ok(dict)
674    }
675}
676
677impl Form {
678    pub(crate) fn from_attributes(
679        attrs: std::collections::HashMap<&str, &str>,
680    ) -> Result<Self, crate::errors::Error> {
681        let name = attrs.get("name").copied().unwrap_or_default().to_string();
682
683        let last_modified = if let Some(lm) = attrs.get("lastModified") {
684            if lm.is_empty() {
685                None
686            } else {
687                parse_datetime_internal(lm).ok()
688            }
689        } else {
690            None
691        };
692
693        let who_last_modified_name = attrs
694            .get("whoLastModifiedName")
695            .filter(|s| !s.is_empty())
696            .map(|s| s.to_string());
697        let who_last_modified_role = attrs
698            .get("whoLastModifiedRole")
699            .filter(|s| !s.is_empty())
700            .map(|s| s.to_string());
701
702        let when_created = attrs
703            .get("whenCreated")
704            .and_then(|s| s.parse().ok())
705            .unwrap_or(0);
706
707        let has_errors = attrs
708            .get("hasErrors")
709            .map(|s| *s == "true")
710            .unwrap_or(false);
711
712        let has_warnings = attrs
713            .get("hasWarnings")
714            .map(|s| *s == "true")
715            .unwrap_or(false);
716
717        let locked = attrs.get("locked").map(|s| *s == "true").unwrap_or(false);
718
719        let user = attrs
720            .get("user")
721            .filter(|s| !s.is_empty())
722            .map(|s| s.to_string());
723
724        let date_time_changed = if let Some(dtc) = attrs.get("dateTimeChanged") {
725            if dtc.is_empty() {
726                None
727            } else {
728                parse_datetime_internal(dtc).ok()
729            }
730        } else {
731            None
732        };
733
734        let form_title = attrs
735            .get("formTitle")
736            .copied()
737            .unwrap_or_default()
738            .to_string();
739
740        let form_index = attrs
741            .get("formIndex")
742            .and_then(|s| s.parse().ok())
743            .unwrap_or(0);
744
745        let form_group = attrs
746            .get("formGroup")
747            .filter(|s| !s.is_empty())
748            .map(|s| s.to_string());
749        let form_state = attrs
750            .get("formState")
751            .copied()
752            .unwrap_or_default()
753            .to_string();
754
755        Ok(Form {
756            name,
757            last_modified,
758            who_last_modified_name,
759            who_last_modified_role,
760            when_created,
761            has_errors,
762            has_warnings,
763            locked,
764            user,
765            date_time_changed,
766            form_title,
767            form_index,
768            form_group,
769            form_state,
770            states: None,
771            lock_state: None,
772            categories: None,
773        })
774    }
775}
776
777fn parse_datetime_internal(s: &str) -> Result<DateTime<Utc>, crate::errors::Error> {
778    if let Ok(dt) = chrono::DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S %z") {
779        Ok(dt.with_timezone(&Utc))
780    } else if let Ok(dt) = chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%z") {
781        Ok(dt.with_timezone(&Utc))
782    } else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
783        Ok(dt.with_timezone(&Utc))
784    } else {
785        Err(crate::errors::Error::ParsingError(
786            quick_xml::de::DeError::Custom(format!("Invalid datetime format: {}", s)),
787        ))
788    }
789}
790
791#[cfg(not(feature = "python"))]
792#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
793pub struct State {
794    #[serde(rename = "value")]
795    #[serde(alias = "@value")]
796    #[serde(alias = "value")]
797    pub value: String,
798    #[serde(rename = "signer")]
799    #[serde(alias = "@signer")]
800    #[serde(alias = "signer")]
801    pub signer: String,
802    #[serde(rename = "signerUniqueId")]
803    #[serde(alias = "@signerUniqueId")]
804    #[serde(alias = "signerUniqueId")]
805    pub signer_unique_id: String,
806
807    #[serde(rename = "dateSigned")]
808    #[serde(alias = "@dateSigned")]
809    #[serde(alias = "dateSigned")]
810    #[serde(
811        default = "default_datetime_none",
812        deserialize_with = "deserialize_empty_string_as_none_datetime"
813    )]
814    pub date_signed: Option<DateTime<Utc>>,
815}
816
817#[cfg(feature = "python")]
818#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
819#[pyclass(skip_from_py_object)]
820pub struct State {
821    #[serde(rename = "value")]
822    #[serde(alias = "@value")]
823    #[serde(alias = "value")]
824    pub value: String,
825    #[serde(rename = "signer")]
826    #[serde(alias = "@signer")]
827    #[serde(alias = "signer")]
828    pub signer: String,
829    #[serde(rename = "signerUniqueId")]
830    #[serde(alias = "@signerUniqueId")]
831    #[serde(alias = "signerUniqueId")]
832    pub signer_unique_id: String,
833
834    #[serde(rename = "dateSigned")]
835    #[serde(alias = "@dateSigned")]
836    #[serde(alias = "dateSigned")]
837    #[serde(
838        default = "default_datetime_none",
839        deserialize_with = "deserialize_empty_string_as_none_datetime"
840    )]
841    pub date_signed: Option<DateTime<Utc>>,
842}
843
844#[cfg(feature = "python")]
845#[pymethods]
846impl State {
847    #[getter]
848    fn value(&self) -> PyResult<String> {
849        Ok(self.value.clone())
850    }
851
852    #[getter]
853    fn signer(&self) -> PyResult<String> {
854        Ok(self.signer.clone())
855    }
856
857    #[getter]
858    fn signer_unique_id(&self) -> PyResult<String> {
859        Ok(self.signer_unique_id.clone())
860    }
861
862    #[getter]
863    fn date_signed<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
864        to_py_datetime_option(py, &self.date_signed)
865    }
866
867    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
868        let dict = PyDict::new(py);
869        dict.set_item("value", &self.value)?;
870        dict.set_item("signer", &self.signer)?;
871        dict.set_item("signer_unique_id", &self.signer_unique_id)?;
872        dict.set_item("date_signed", to_py_datetime_option(py, &self.date_signed)?)?;
873
874        Ok(dict)
875    }
876}
877
878#[cfg(not(feature = "python"))]
879#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
880pub struct LockState {
881    #[serde(rename = "locked")]
882    #[serde(alias = "@locked")]
883    #[serde(alias = "locked")]
884    pub locked: bool,
885
886    #[serde(rename = "user")]
887    #[serde(alias = "@user")]
888    #[serde(alias = "user")]
889    #[serde(
890        default = "default_string_none",
891        deserialize_with = "deserialize_empty_string_as_none"
892    )]
893    pub user: Option<String>,
894
895    #[serde(rename = "userUniqueId")]
896    #[serde(alias = "@userUniqueId")]
897    #[serde(alias = "userUniqueId")]
898    #[serde(
899        default = "default_string_none",
900        deserialize_with = "deserialize_empty_string_as_none"
901    )]
902    pub user_unique_id: Option<String>,
903
904    #[serde(rename = "dateTimeChanged")]
905    #[serde(alias = "@dateTimeChanged")]
906    #[serde(alias = "dateTimeChanged")]
907    #[serde(
908        default = "default_datetime_none",
909        deserialize_with = "deserialize_empty_string_as_none_datetime"
910    )]
911    pub date_time_changed: Option<DateTime<Utc>>,
912}
913
914#[cfg(feature = "python")]
915#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
916#[pyclass(skip_from_py_object)]
917pub struct LockState {
918    #[serde(rename = "locked")]
919    #[serde(alias = "@locked")]
920    #[serde(alias = "locked")]
921    pub locked: bool,
922
923    #[serde(rename = "user")]
924    #[serde(alias = "@user")]
925    #[serde(alias = "user")]
926    #[serde(
927        default = "default_string_none",
928        deserialize_with = "deserialize_empty_string_as_none"
929    )]
930    pub user: Option<String>,
931
932    #[serde(rename = "userUniqueId")]
933    #[serde(alias = "@userUniqueId")]
934    #[serde(alias = "userUniqueId")]
935    #[serde(
936        default = "default_string_none",
937        deserialize_with = "deserialize_empty_string_as_none"
938    )]
939    pub user_unique_id: Option<String>,
940
941    #[serde(rename = "dateTimeChanged")]
942    #[serde(alias = "@dateTimeChanged")]
943    #[serde(alias = "dateTimeChanged")]
944    #[serde(
945        default = "default_datetime_none",
946        deserialize_with = "deserialize_empty_string_as_none_datetime"
947    )]
948    pub date_time_changed: Option<DateTime<Utc>>,
949}
950
951#[cfg(feature = "python")]
952#[pymethods]
953impl LockState {
954    #[getter]
955    fn locked(&self) -> PyResult<bool> {
956        Ok(self.locked)
957    }
958
959    #[getter]
960    fn user(&self) -> PyResult<Option<String>> {
961        Ok(self.user.clone())
962    }
963
964    #[getter]
965    fn user_unique_id(&self) -> PyResult<Option<String>> {
966        Ok(self.user_unique_id.clone())
967    }
968
969    #[getter]
970    fn date_time_changed<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
971        to_py_datetime_option(py, &self.date_time_changed)
972    }
973
974    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
975        let dict = PyDict::new(py);
976        dict.set_item("locked", self.locked)?;
977        dict.set_item("user", &self.user)?;
978        dict.set_item("user_unique_id", &self.user_unique_id)?;
979        dict.set_item(
980            "date_time_changed",
981            to_py_datetime_option(py, &self.date_time_changed)?,
982        )?;
983
984        Ok(dict)
985    }
986}
987
988#[cfg(not(feature = "python"))]
989#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
990pub struct Form {
991    #[serde(rename = "name")]
992    #[serde(alias = "@name")]
993    #[serde(alias = "name")]
994    pub name: String,
995
996    #[serde(rename = "lastModified")]
997    #[serde(alias = "@lastModified")]
998    #[serde(alias = "lastModified")]
999    #[serde(
1000        default = "default_datetime_none",
1001        deserialize_with = "deserialize_empty_string_as_none_datetime"
1002    )]
1003    pub last_modified: Option<DateTime<Utc>>,
1004
1005    #[serde(rename = "whoLastModifiedName")]
1006    #[serde(alias = "@whoLastModifiedName")]
1007    #[serde(alias = "whoLastModifiedName")]
1008    #[serde(
1009        default = "default_string_none",
1010        deserialize_with = "deserialize_empty_string_as_none"
1011    )]
1012    pub who_last_modified_name: Option<String>,
1013
1014    #[serde(rename = "whoLastModifiedRole")]
1015    #[serde(alias = "@whoLastModifiedRole")]
1016    #[serde(alias = "whoLastModifiedRole")]
1017    #[serde(
1018        default = "default_string_none",
1019        deserialize_with = "deserialize_empty_string_as_none"
1020    )]
1021    pub who_last_modified_role: Option<String>,
1022
1023    #[serde(rename = "whenCreated")]
1024    #[serde(alias = "@whenCreated")]
1025    #[serde(alias = "whenCreated")]
1026    pub when_created: usize,
1027    #[serde(rename = "hasErrors")]
1028    #[serde(alias = "@hasErrors")]
1029    #[serde(alias = "hasErrors")]
1030    pub has_errors: bool,
1031    #[serde(rename = "hasWarnings")]
1032    #[serde(alias = "@hasWarnings")]
1033    #[serde(alias = "hasWarnings")]
1034    pub has_warnings: bool,
1035    #[serde(rename = "locked")]
1036    #[serde(alias = "@locked")]
1037    #[serde(alias = "locked")]
1038    pub locked: bool,
1039
1040    #[serde(rename = "user")]
1041    #[serde(alias = "@user")]
1042    #[serde(alias = "user")]
1043    #[serde(
1044        default = "default_string_none",
1045        deserialize_with = "deserialize_empty_string_as_none"
1046    )]
1047    pub user: Option<String>,
1048
1049    #[serde(rename = "dateTimeChanged")]
1050    #[serde(alias = "@dateTimeChanged")]
1051    #[serde(alias = "dateTimeChanged")]
1052    #[serde(
1053        default = "default_datetime_none",
1054        deserialize_with = "deserialize_empty_string_as_none_datetime"
1055    )]
1056    pub date_time_changed: Option<DateTime<Utc>>,
1057
1058    #[serde(rename = "formTitle")]
1059    #[serde(alias = "@formTitle")]
1060    #[serde(alias = "formTitle")]
1061    pub form_title: String,
1062    #[serde(rename = "formIndex")]
1063    #[serde(alias = "@formIndex")]
1064    #[serde(alias = "formIndex")]
1065    pub form_index: usize,
1066
1067    #[serde(rename = "formGroup")]
1068    #[serde(alias = "@formGroup")]
1069    #[serde(alias = "formGroup")]
1070    #[serde(
1071        default = "default_string_none",
1072        deserialize_with = "deserialize_empty_string_as_none"
1073    )]
1074    pub form_group: Option<String>,
1075
1076    #[serde(rename = "formState")]
1077    #[serde(alias = "@formState")]
1078    #[serde(alias = "formState")]
1079    pub form_state: String,
1080
1081    #[serde(alias = "state")]
1082    pub states: Option<Vec<State>>,
1083
1084    #[serde(alias = "lockState")]
1085    pub lock_state: Option<LockState>,
1086
1087    #[serde(alias = "category")]
1088    pub categories: Option<Vec<Category>>,
1089}
1090
1091#[cfg(feature = "python")]
1092#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1093#[pyclass(skip_from_py_object)]
1094pub struct Form {
1095    #[serde(rename = "name")]
1096    #[serde(alias = "@name")]
1097    #[serde(alias = "name")]
1098    pub name: String,
1099
1100    #[serde(rename = "lastModified")]
1101    #[serde(alias = "@lastModified")]
1102    #[serde(alias = "lastModified")]
1103    #[serde(
1104        default = "default_datetime_none",
1105        deserialize_with = "deserialize_empty_string_as_none_datetime"
1106    )]
1107    pub last_modified: Option<DateTime<Utc>>,
1108
1109    #[serde(rename = "whoLastModifiedName")]
1110    #[serde(alias = "@whoLastModifiedName")]
1111    #[serde(alias = "whoLastModifiedName")]
1112    #[serde(
1113        default = "default_string_none",
1114        deserialize_with = "deserialize_empty_string_as_none"
1115    )]
1116    pub who_last_modified_name: Option<String>,
1117
1118    #[serde(rename = "whoLastModifiedRole")]
1119    #[serde(alias = "@whoLastModifiedRole")]
1120    #[serde(alias = "whoLastModifiedRole")]
1121    #[serde(
1122        default = "default_string_none",
1123        deserialize_with = "deserialize_empty_string_as_none"
1124    )]
1125    pub who_last_modified_role: Option<String>,
1126
1127    #[serde(rename = "whenCreated")]
1128    #[serde(alias = "@whenCreated")]
1129    #[serde(alias = "whenCreated")]
1130    pub when_created: usize,
1131    #[serde(rename = "hasErrors")]
1132    #[serde(alias = "@hasErrors")]
1133    #[serde(alias = "hasErrors")]
1134    pub has_errors: bool,
1135    #[serde(rename = "hasWarnings")]
1136    #[serde(alias = "@hasWarnings")]
1137    #[serde(alias = "hasWarnings")]
1138    pub has_warnings: bool,
1139    #[serde(rename = "locked")]
1140    #[serde(alias = "@locked")]
1141    #[serde(alias = "locked")]
1142    pub locked: bool,
1143
1144    #[serde(rename = "user")]
1145    #[serde(alias = "@user")]
1146    #[serde(alias = "user")]
1147    #[serde(
1148        default = "default_string_none",
1149        deserialize_with = "deserialize_empty_string_as_none"
1150    )]
1151    pub user: Option<String>,
1152
1153    #[serde(rename = "dateTimeChanged")]
1154    #[serde(alias = "@dateTimeChanged")]
1155    #[serde(alias = "dateTimeChanged")]
1156    #[serde(
1157        default = "default_datetime_none",
1158        deserialize_with = "deserialize_empty_string_as_none_datetime"
1159    )]
1160    pub date_time_changed: Option<DateTime<Utc>>,
1161
1162    #[serde(rename = "formTitle")]
1163    #[serde(alias = "@formTitle")]
1164    #[serde(alias = "formTitle")]
1165    pub form_title: String,
1166    #[serde(rename = "formIndex")]
1167    #[serde(alias = "@formIndex")]
1168    #[serde(alias = "formIndex")]
1169    pub form_index: usize,
1170
1171    #[serde(rename = "formGroup")]
1172    #[serde(alias = "@formGroup")]
1173    #[serde(alias = "formGroup")]
1174    #[serde(
1175        default = "default_string_none",
1176        deserialize_with = "deserialize_empty_string_as_none"
1177    )]
1178    pub form_group: Option<String>,
1179
1180    #[serde(rename = "formState")]
1181    #[serde(alias = "@formState")]
1182    #[serde(alias = "formState")]
1183    pub form_state: String,
1184
1185    #[serde(alias = "state")]
1186    pub states: Option<Vec<State>>,
1187
1188    #[serde(alias = "lockState")]
1189    pub lock_state: Option<LockState>,
1190
1191    #[serde(alias = "category")]
1192    pub categories: Option<Vec<Category>>,
1193}
1194
1195#[cfg(feature = "python")]
1196#[pymethods]
1197impl Form {
1198    #[getter]
1199    fn name(&self) -> PyResult<String> {
1200        Ok(self.name.clone())
1201    }
1202
1203    #[getter]
1204    fn last_modified<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
1205        to_py_datetime_option(py, &self.last_modified)
1206    }
1207
1208    #[getter]
1209    fn who_last_modified_name(&self) -> PyResult<Option<String>> {
1210        Ok(self.who_last_modified_name.clone())
1211    }
1212
1213    #[getter]
1214    fn who_last_modified_role(&self) -> PyResult<Option<String>> {
1215        Ok(self.who_last_modified_role.clone())
1216    }
1217
1218    #[getter]
1219    fn when_created(&self) -> PyResult<usize> {
1220        Ok(self.when_created)
1221    }
1222
1223    #[getter]
1224    fn has_errors(&self) -> PyResult<bool> {
1225        Ok(self.has_errors)
1226    }
1227
1228    #[getter]
1229    fn has_warnings(&self) -> PyResult<bool> {
1230        Ok(self.has_warnings)
1231    }
1232
1233    #[getter]
1234    fn locked(&self) -> PyResult<bool> {
1235        Ok(self.locked)
1236    }
1237
1238    #[getter]
1239    fn user(&self) -> PyResult<Option<String>> {
1240        Ok(self.user.clone())
1241    }
1242
1243    #[getter]
1244    fn date_time_changed<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
1245        to_py_datetime_option(py, &self.date_time_changed)
1246    }
1247
1248    #[getter]
1249    fn form_title(&self) -> PyResult<String> {
1250        Ok(self.form_title.clone())
1251    }
1252
1253    #[getter]
1254    fn form_index(&self) -> PyResult<usize> {
1255        Ok(self.form_index)
1256    }
1257
1258    #[getter]
1259    fn form_group(&self) -> PyResult<Option<String>> {
1260        Ok(self.form_group.clone())
1261    }
1262
1263    #[getter]
1264    fn form_state(&self) -> PyResult<String> {
1265        Ok(self.form_state.clone())
1266    }
1267
1268    #[getter]
1269    fn states(&self) -> PyResult<Option<Vec<State>>> {
1270        Ok(self.states.clone())
1271    }
1272
1273    #[getter]
1274    fn lock_state(&self) -> PyResult<Option<LockState>> {
1275        Ok(self.lock_state.clone())
1276    }
1277
1278    #[getter]
1279    fn categories(&self) -> PyResult<Option<Vec<Category>>> {
1280        Ok(self.categories.clone())
1281    }
1282
1283    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
1284        let dict = PyDict::new(py);
1285        dict.set_item("name", &self.name)?;
1286        dict.set_item(
1287            "last_modified",
1288            to_py_datetime_option(py, &self.last_modified)?,
1289        )?;
1290        dict.set_item("who_last_modified_name", &self.who_last_modified_name)?;
1291        dict.set_item("who_last_modified_role", &self.who_last_modified_role)?;
1292        dict.set_item("when_created", self.when_created)?;
1293        dict.set_item("has_errors", self.has_errors)?;
1294        dict.set_item("has_warnings", self.has_warnings)?;
1295        dict.set_item("locked", self.locked)?;
1296        dict.set_item("user", &self.user)?;
1297        dict.set_item(
1298            "date_time_changed",
1299            to_py_datetime_option(py, &self.date_time_changed)?,
1300        )?;
1301        dict.set_item("form_title", &self.form_title)?;
1302        dict.set_item("form_index", self.form_index)?;
1303        dict.set_item("form_group", &self.form_group)?;
1304        dict.set_item("form_state", &self.form_state)?;
1305
1306        let mut state_dicts = Vec::new();
1307        if let Some(states) = &self.states {
1308            for state in states {
1309                let state_dict = state.to_dict(py)?;
1310                state_dicts.push(state_dict);
1311            }
1312            dict.set_item("states", state_dicts)?;
1313        } else {
1314            dict.set_item("states", py.None())?;
1315        }
1316
1317        if let Some(lock_state) = &self.lock_state {
1318            dict.set_item("lock_state", lock_state.to_dict(py)?)?;
1319        } else {
1320            dict.set_item("lock_state", py.None())?;
1321        }
1322
1323        if let Some(categories) = &self.categories {
1324            let mut category_dicts = Vec::new();
1325            for category in categories {
1326                let category_dict = category.to_dict(py)?;
1327                category_dicts.push(category_dict);
1328            }
1329            dict.set_item("categories", category_dicts)?;
1330        } else {
1331            dict.set_item("categories", py.None())?;
1332        }
1333
1334        Ok(dict)
1335    }
1336}
1337
1338impl State {
1339    pub(crate) fn from_attributes(
1340        attrs: std::collections::HashMap<&str, &str>,
1341    ) -> Result<Self, crate::errors::Error> {
1342        let value = attrs.get("value").copied().unwrap_or_default().to_string();
1343        let signer = attrs.get("signer").copied().unwrap_or_default().to_string();
1344        let signer_unique_id = attrs
1345            .get("signerUniqueId")
1346            .copied()
1347            .unwrap_or_default()
1348            .to_string();
1349
1350        let date_signed = if let Some(ds) = attrs.get("dateSigned") {
1351            if ds.is_empty() {
1352                None
1353            } else {
1354                parse_datetime_internal(ds).ok()
1355            }
1356        } else {
1357            None
1358        };
1359
1360        Ok(State {
1361            value,
1362            signer,
1363            signer_unique_id,
1364            date_signed,
1365        })
1366    }
1367}
1368
1369impl LockState {
1370    pub(crate) fn from_attributes(
1371        attrs: std::collections::HashMap<&str, &str>,
1372    ) -> Result<Self, crate::errors::Error> {
1373        let locked = attrs.get("locked").map(|s| *s == "true").unwrap_or(false);
1374        let user = attrs
1375            .get("user")
1376            .filter(|s| !s.is_empty())
1377            .map(|s| s.to_string());
1378        let user_unique_id = attrs
1379            .get("userUniqueId")
1380            .filter(|s| !s.is_empty())
1381            .map(|s| s.to_string());
1382
1383        let date_time_changed = if let Some(dtc) = attrs.get("dateTimeChanged") {
1384            if dtc.is_empty() {
1385                None
1386            } else {
1387                parse_datetime_internal(dtc).ok()
1388            }
1389        } else {
1390            None
1391        };
1392
1393        Ok(LockState {
1394            locked,
1395            user,
1396            user_unique_id,
1397            date_time_changed,
1398        })
1399    }
1400}
1401
1402impl Category {
1403    pub(crate) fn from_attributes(
1404        attrs: std::collections::HashMap<&str, &str>,
1405    ) -> Result<Self, crate::errors::Error> {
1406        let name = attrs.get("name").copied().unwrap_or_default().to_string();
1407        let category_type = attrs.get("type").copied().unwrap_or_default().to_string();
1408        let highest_index = attrs
1409            .get("highestIndex")
1410            .and_then(|s| s.parse().ok())
1411            .unwrap_or(0);
1412
1413        Ok(Category {
1414            name,
1415            category_type,
1416            highest_index,
1417            fields: None,
1418        })
1419    }
1420}
1421
1422impl Field {
1423    pub(crate) fn from_attributes(
1424        attrs: std::collections::HashMap<&str, &str>,
1425    ) -> Result<Self, crate::errors::Error> {
1426        let name = attrs.get("name").copied().unwrap_or_default().to_string();
1427        let field_type = attrs.get("type").copied().unwrap_or_default().to_string();
1428        let data_type = attrs
1429            .get("dataType")
1430            .filter(|s| !s.is_empty())
1431            .map(|s| s.to_string());
1432        let error_code = attrs
1433            .get("errorCode")
1434            .copied()
1435            .unwrap_or_default()
1436            .to_string();
1437
1438        let when_created = if let Some(wc) = attrs.get("whenCreated") {
1439            if wc.is_empty() {
1440                None
1441            } else {
1442                Some(parse_datetime_internal(wc)?)
1443            }
1444        } else {
1445            None
1446        };
1447
1448        let keep_history = attrs
1449            .get("keepHistory")
1450            .map(|s| *s == "true")
1451            .unwrap_or(false);
1452
1453        Ok(Field {
1454            name,
1455            field_type,
1456            data_type,
1457            error_code,
1458            when_created,
1459            keep_history,
1460            entries: None,
1461            comments: None,
1462        })
1463    }
1464}
1465
1466impl Entry {
1467    pub(crate) fn from_attributes(
1468        attrs: std::collections::HashMap<&str, &str>,
1469    ) -> Result<Self, crate::errors::Error> {
1470        let entry_id = attrs
1471            .get("id")
1472            .or_else(|| attrs.get("entryId"))
1473            .copied()
1474            .unwrap_or_default()
1475            .to_string();
1476
1477        let reviewed_by = attrs
1478            .get("reviewedBy")
1479            .filter(|s| !s.is_empty())
1480            .map(|s| s.to_string());
1481        let reviewed_by_unique_id = attrs
1482            .get("reviewedByUniqueId")
1483            .filter(|s| !s.is_empty())
1484            .map(|s| s.to_string());
1485
1486        let reviewed_by_when = if let Some(rbw) = attrs.get("reviewedByWhen") {
1487            if rbw.is_empty() {
1488                None
1489            } else {
1490                parse_datetime_internal(rbw).ok()
1491            }
1492        } else {
1493            None
1494        };
1495
1496        Ok(Entry {
1497            entry_id,
1498            reviewed_by,
1499            reviewed_by_unique_id,
1500            reviewed_by_when,
1501            value: None,
1502            reason: None,
1503        })
1504    }
1505}
1506
1507impl Value {
1508    pub(crate) fn from_attributes(
1509        attrs: std::collections::HashMap<&str, &str>,
1510    ) -> Result<Self, crate::errors::Error> {
1511        let by = attrs.get("by").copied().unwrap_or_default().to_string();
1512        let by_unique_id = attrs
1513            .get("byUniqueId")
1514            .filter(|s| !s.is_empty())
1515            .map(|s| s.to_string());
1516        let role = attrs.get("role").copied().unwrap_or_default().to_string();
1517
1518        let when = if let Some(w) = attrs.get("when") {
1519            if w.is_empty() {
1520                None
1521            } else {
1522                Some(parse_datetime_internal(w)?)
1523            }
1524        } else {
1525            None
1526        };
1527
1528        Ok(Value {
1529            by,
1530            by_unique_id,
1531            role,
1532            when,
1533            value: String::new(),
1534        })
1535    }
1536}
1537
1538impl Reason {
1539    pub(crate) fn from_attributes(
1540        attrs: std::collections::HashMap<&str, &str>,
1541    ) -> Result<Self, crate::errors::Error> {
1542        let by = attrs.get("by").copied().unwrap_or_default().to_string();
1543        let by_unique_id = attrs
1544            .get("byUniqueId")
1545            .filter(|s| !s.is_empty())
1546            .map(|s| s.to_string());
1547        let role = attrs.get("role").copied().unwrap_or_default().to_string();
1548
1549        let when = if let Some(w) = attrs.get("when") {
1550            if w.is_empty() {
1551                None
1552            } else {
1553                Some(parse_datetime_internal(w)?)
1554            }
1555        } else {
1556            None
1557        };
1558
1559        Ok(Reason {
1560            by,
1561            by_unique_id,
1562            role,
1563            when,
1564            value: String::new(),
1565        })
1566    }
1567}