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]
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]
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]
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)]
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]
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)]
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 fn from_attributes(
679        attrs: std::collections::HashMap<String, String>,
680    ) -> Result<Self, crate::errors::Error> {
681        let name = attrs.get("name").cloned().unwrap_or_default();
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            .cloned();
697        let who_last_modified_role = attrs
698            .get("whoLastModifiedRole")
699            .filter(|s| !s.is_empty())
700            .cloned();
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.get("hasErrors").map(|s| s == "true").unwrap_or(false);
708
709        let has_warnings = attrs
710            .get("hasWarnings")
711            .map(|s| s == "true")
712            .unwrap_or(false);
713
714        let locked = attrs.get("locked").map(|s| s == "true").unwrap_or(false);
715
716        let user = attrs.get("user").filter(|s| !s.is_empty()).cloned();
717
718        let date_time_changed = if let Some(dtc) = attrs.get("dateTimeChanged") {
719            if dtc.is_empty() {
720                None
721            } else {
722                parse_datetime_internal(dtc).ok()
723            }
724        } else {
725            None
726        };
727
728        let form_title = attrs.get("formTitle").cloned().unwrap_or_default();
729
730        let form_index = attrs
731            .get("formIndex")
732            .and_then(|s| s.parse().ok())
733            .unwrap_or(0);
734
735        let form_group = attrs.get("formGroup").filter(|s| !s.is_empty()).cloned();
736        let form_state = attrs.get("formState").cloned().unwrap_or_default();
737
738        Ok(Form {
739            name,
740            last_modified,
741            who_last_modified_name,
742            who_last_modified_role,
743            when_created,
744            has_errors,
745            has_warnings,
746            locked,
747            user,
748            date_time_changed,
749            form_title,
750            form_index,
751            form_group,
752            form_state,
753            states: None,
754            lock_state: None,
755            categories: None,
756        })
757    }
758}
759
760fn parse_datetime_internal(s: &str) -> Result<DateTime<Utc>, crate::errors::Error> {
761    if let Ok(dt) = chrono::DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S %z") {
762        Ok(dt.with_timezone(&Utc))
763    } else if let Ok(dt) = chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%z") {
764        Ok(dt.with_timezone(&Utc))
765    } else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
766        Ok(dt.with_timezone(&Utc))
767    } else {
768        Err(crate::errors::Error::ParsingError(
769            quick_xml::de::DeError::Custom(format!("Invalid datetime format: {}", s)),
770        ))
771    }
772}
773
774#[cfg(not(feature = "python"))]
775#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
776pub struct State {
777    #[serde(rename = "value")]
778    #[serde(alias = "@value")]
779    #[serde(alias = "value")]
780    pub value: String,
781    #[serde(rename = "signer")]
782    #[serde(alias = "@signer")]
783    #[serde(alias = "signer")]
784    pub signer: String,
785    #[serde(rename = "signerUniqueId")]
786    #[serde(alias = "@signerUniqueId")]
787    #[serde(alias = "signerUniqueId")]
788    pub signer_unique_id: String,
789
790    #[serde(rename = "dateSigned")]
791    #[serde(alias = "@dateSigned")]
792    #[serde(alias = "dateSigned")]
793    #[serde(
794        default = "default_datetime_none",
795        deserialize_with = "deserialize_empty_string_as_none_datetime"
796    )]
797    pub date_signed: Option<DateTime<Utc>>,
798}
799
800#[cfg(feature = "python")]
801#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
802#[pyclass]
803pub struct State {
804    #[serde(rename = "value")]
805    #[serde(alias = "@value")]
806    #[serde(alias = "value")]
807    pub value: String,
808    #[serde(rename = "signer")]
809    #[serde(alias = "@signer")]
810    #[serde(alias = "signer")]
811    pub signer: String,
812    #[serde(rename = "signerUniqueId")]
813    #[serde(alias = "@signerUniqueId")]
814    #[serde(alias = "signerUniqueId")]
815    pub signer_unique_id: String,
816
817    #[serde(rename = "dateSigned")]
818    #[serde(alias = "@dateSigned")]
819    #[serde(alias = "dateSigned")]
820    #[serde(
821        default = "default_datetime_none",
822        deserialize_with = "deserialize_empty_string_as_none_datetime"
823    )]
824    pub date_signed: Option<DateTime<Utc>>,
825}
826
827#[cfg(feature = "python")]
828#[pymethods]
829impl State {
830    #[getter]
831    fn value(&self) -> PyResult<String> {
832        Ok(self.value.clone())
833    }
834
835    #[getter]
836    fn signer(&self) -> PyResult<String> {
837        Ok(self.signer.clone())
838    }
839
840    #[getter]
841    fn signer_unique_id(&self) -> PyResult<String> {
842        Ok(self.signer_unique_id.clone())
843    }
844
845    #[getter]
846    fn date_signed<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
847        to_py_datetime_option(py, &self.date_signed)
848    }
849
850    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
851        let dict = PyDict::new(py);
852        dict.set_item("value", &self.value)?;
853        dict.set_item("signer", &self.signer)?;
854        dict.set_item("signer_unique_id", &self.signer_unique_id)?;
855        dict.set_item("date_signed", to_py_datetime_option(py, &self.date_signed)?)?;
856
857        Ok(dict)
858    }
859}
860
861#[cfg(not(feature = "python"))]
862#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
863pub struct LockState {
864    #[serde(rename = "locked")]
865    #[serde(alias = "@locked")]
866    #[serde(alias = "locked")]
867    pub locked: bool,
868
869    #[serde(rename = "user")]
870    #[serde(alias = "@user")]
871    #[serde(alias = "user")]
872    #[serde(
873        default = "default_string_none",
874        deserialize_with = "deserialize_empty_string_as_none"
875    )]
876    pub user: Option<String>,
877
878    #[serde(rename = "userUniqueId")]
879    #[serde(alias = "@userUniqueId")]
880    #[serde(alias = "userUniqueId")]
881    #[serde(
882        default = "default_string_none",
883        deserialize_with = "deserialize_empty_string_as_none"
884    )]
885    pub user_unique_id: Option<String>,
886
887    #[serde(rename = "dateTimeChanged")]
888    #[serde(alias = "@dateTimeChanged")]
889    #[serde(alias = "dateTimeChanged")]
890    #[serde(
891        default = "default_datetime_none",
892        deserialize_with = "deserialize_empty_string_as_none_datetime"
893    )]
894    pub date_time_changed: Option<DateTime<Utc>>,
895}
896
897#[cfg(feature = "python")]
898#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
899#[pyclass]
900pub struct LockState {
901    #[serde(rename = "locked")]
902    #[serde(alias = "@locked")]
903    #[serde(alias = "locked")]
904    pub locked: bool,
905
906    #[serde(rename = "user")]
907    #[serde(alias = "@user")]
908    #[serde(alias = "user")]
909    #[serde(
910        default = "default_string_none",
911        deserialize_with = "deserialize_empty_string_as_none"
912    )]
913    pub user: Option<String>,
914
915    #[serde(rename = "userUniqueId")]
916    #[serde(alias = "@userUniqueId")]
917    #[serde(alias = "userUniqueId")]
918    #[serde(
919        default = "default_string_none",
920        deserialize_with = "deserialize_empty_string_as_none"
921    )]
922    pub user_unique_id: Option<String>,
923
924    #[serde(rename = "dateTimeChanged")]
925    #[serde(alias = "@dateTimeChanged")]
926    #[serde(alias = "dateTimeChanged")]
927    #[serde(
928        default = "default_datetime_none",
929        deserialize_with = "deserialize_empty_string_as_none_datetime"
930    )]
931    pub date_time_changed: Option<DateTime<Utc>>,
932}
933
934#[cfg(feature = "python")]
935#[pymethods]
936impl LockState {
937    #[getter]
938    fn locked(&self) -> PyResult<bool> {
939        Ok(self.locked)
940    }
941
942    #[getter]
943    fn user(&self) -> PyResult<Option<String>> {
944        Ok(self.user.clone())
945    }
946
947    #[getter]
948    fn user_unique_id(&self) -> PyResult<Option<String>> {
949        Ok(self.user_unique_id.clone())
950    }
951
952    #[getter]
953    fn date_time_changed<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
954        to_py_datetime_option(py, &self.date_time_changed)
955    }
956
957    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
958        let dict = PyDict::new(py);
959        dict.set_item("locked", self.locked)?;
960        dict.set_item("user", &self.user)?;
961        dict.set_item("user_unique_id", &self.user_unique_id)?;
962        dict.set_item(
963            "date_time_changed",
964            to_py_datetime_option(py, &self.date_time_changed)?,
965        )?;
966
967        Ok(dict)
968    }
969}
970
971#[cfg(not(feature = "python"))]
972#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
973pub struct Form {
974    #[serde(rename = "name")]
975    #[serde(alias = "@name")]
976    #[serde(alias = "name")]
977    pub name: String,
978
979    #[serde(rename = "lastModified")]
980    #[serde(alias = "@lastModified")]
981    #[serde(alias = "lastModified")]
982    #[serde(
983        default = "default_datetime_none",
984        deserialize_with = "deserialize_empty_string_as_none_datetime"
985    )]
986    pub last_modified: Option<DateTime<Utc>>,
987
988    #[serde(rename = "whoLastModifiedName")]
989    #[serde(alias = "@whoLastModifiedName")]
990    #[serde(alias = "whoLastModifiedName")]
991    #[serde(
992        default = "default_string_none",
993        deserialize_with = "deserialize_empty_string_as_none"
994    )]
995    pub who_last_modified_name: Option<String>,
996
997    #[serde(rename = "whoLastModifiedRole")]
998    #[serde(alias = "@whoLastModifiedRole")]
999    #[serde(alias = "whoLastModifiedRole")]
1000    #[serde(
1001        default = "default_string_none",
1002        deserialize_with = "deserialize_empty_string_as_none"
1003    )]
1004    pub who_last_modified_role: Option<String>,
1005
1006    #[serde(rename = "whenCreated")]
1007    #[serde(alias = "@whenCreated")]
1008    #[serde(alias = "whenCreated")]
1009    pub when_created: usize,
1010    #[serde(rename = "hasErrors")]
1011    #[serde(alias = "@hasErrors")]
1012    #[serde(alias = "hasErrors")]
1013    pub has_errors: bool,
1014    #[serde(rename = "hasWarnings")]
1015    #[serde(alias = "@hasWarnings")]
1016    #[serde(alias = "hasWarnings")]
1017    pub has_warnings: bool,
1018    #[serde(rename = "locked")]
1019    #[serde(alias = "@locked")]
1020    #[serde(alias = "locked")]
1021    pub locked: bool,
1022
1023    #[serde(rename = "user")]
1024    #[serde(alias = "@user")]
1025    #[serde(alias = "user")]
1026    #[serde(
1027        default = "default_string_none",
1028        deserialize_with = "deserialize_empty_string_as_none"
1029    )]
1030    pub user: Option<String>,
1031
1032    #[serde(rename = "dateTimeChanged")]
1033    #[serde(alias = "@dateTimeChanged")]
1034    #[serde(alias = "dateTimeChanged")]
1035    #[serde(
1036        default = "default_datetime_none",
1037        deserialize_with = "deserialize_empty_string_as_none_datetime"
1038    )]
1039    pub date_time_changed: Option<DateTime<Utc>>,
1040
1041    #[serde(rename = "formTitle")]
1042    #[serde(alias = "@formTitle")]
1043    #[serde(alias = "formTitle")]
1044    pub form_title: String,
1045    #[serde(rename = "formIndex")]
1046    #[serde(alias = "@formIndex")]
1047    #[serde(alias = "formIndex")]
1048    pub form_index: usize,
1049
1050    #[serde(rename = "formGroup")]
1051    #[serde(alias = "@formGroup")]
1052    #[serde(alias = "formGroup")]
1053    #[serde(
1054        default = "default_string_none",
1055        deserialize_with = "deserialize_empty_string_as_none"
1056    )]
1057    pub form_group: Option<String>,
1058
1059    #[serde(rename = "formState")]
1060    #[serde(alias = "@formState")]
1061    #[serde(alias = "formState")]
1062    pub form_state: String,
1063
1064    #[serde(alias = "state")]
1065    pub states: Option<Vec<State>>,
1066
1067    #[serde(alias = "lockState")]
1068    pub lock_state: Option<LockState>,
1069
1070    #[serde(alias = "category")]
1071    pub categories: Option<Vec<Category>>,
1072}
1073
1074#[cfg(feature = "python")]
1075#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
1076#[pyclass]
1077pub struct Form {
1078    #[serde(rename = "name")]
1079    #[serde(alias = "@name")]
1080    #[serde(alias = "name")]
1081    pub name: String,
1082
1083    #[serde(rename = "lastModified")]
1084    #[serde(alias = "@lastModified")]
1085    #[serde(alias = "lastModified")]
1086    #[serde(
1087        default = "default_datetime_none",
1088        deserialize_with = "deserialize_empty_string_as_none_datetime"
1089    )]
1090    pub last_modified: Option<DateTime<Utc>>,
1091
1092    #[serde(rename = "whoLastModifiedName")]
1093    #[serde(alias = "@whoLastModifiedName")]
1094    #[serde(alias = "whoLastModifiedName")]
1095    #[serde(
1096        default = "default_string_none",
1097        deserialize_with = "deserialize_empty_string_as_none"
1098    )]
1099    pub who_last_modified_name: Option<String>,
1100
1101    #[serde(rename = "whoLastModifiedRole")]
1102    #[serde(alias = "@whoLastModifiedRole")]
1103    #[serde(alias = "whoLastModifiedRole")]
1104    #[serde(
1105        default = "default_string_none",
1106        deserialize_with = "deserialize_empty_string_as_none"
1107    )]
1108    pub who_last_modified_role: Option<String>,
1109
1110    #[serde(rename = "whenCreated")]
1111    #[serde(alias = "@whenCreated")]
1112    #[serde(alias = "whenCreated")]
1113    pub when_created: usize,
1114    #[serde(rename = "hasErrors")]
1115    #[serde(alias = "@hasErrors")]
1116    #[serde(alias = "hasErrors")]
1117    pub has_errors: bool,
1118    #[serde(rename = "hasWarnings")]
1119    #[serde(alias = "@hasWarnings")]
1120    #[serde(alias = "hasWarnings")]
1121    pub has_warnings: bool,
1122    #[serde(rename = "locked")]
1123    #[serde(alias = "@locked")]
1124    #[serde(alias = "locked")]
1125    pub locked: bool,
1126
1127    #[serde(rename = "user")]
1128    #[serde(alias = "@user")]
1129    #[serde(alias = "user")]
1130    #[serde(
1131        default = "default_string_none",
1132        deserialize_with = "deserialize_empty_string_as_none"
1133    )]
1134    pub user: Option<String>,
1135
1136    #[serde(rename = "dateTimeChanged")]
1137    #[serde(alias = "@dateTimeChanged")]
1138    #[serde(alias = "dateTimeChanged")]
1139    #[serde(
1140        default = "default_datetime_none",
1141        deserialize_with = "deserialize_empty_string_as_none_datetime"
1142    )]
1143    pub date_time_changed: Option<DateTime<Utc>>,
1144
1145    #[serde(rename = "formTitle")]
1146    #[serde(alias = "@formTitle")]
1147    #[serde(alias = "formTitle")]
1148    pub form_title: String,
1149    #[serde(rename = "formIndex")]
1150    #[serde(alias = "@formIndex")]
1151    #[serde(alias = "formIndex")]
1152    pub form_index: usize,
1153
1154    #[serde(rename = "formGroup")]
1155    #[serde(alias = "@formGroup")]
1156    #[serde(alias = "formGroup")]
1157    #[serde(
1158        default = "default_string_none",
1159        deserialize_with = "deserialize_empty_string_as_none"
1160    )]
1161    pub form_group: Option<String>,
1162
1163    #[serde(rename = "formState")]
1164    #[serde(alias = "@formState")]
1165    #[serde(alias = "formState")]
1166    pub form_state: String,
1167
1168    #[serde(alias = "state")]
1169    pub states: Option<Vec<State>>,
1170
1171    #[serde(alias = "lockState")]
1172    pub lock_state: Option<LockState>,
1173
1174    #[serde(alias = "category")]
1175    pub categories: Option<Vec<Category>>,
1176}
1177
1178#[cfg(feature = "python")]
1179#[pymethods]
1180impl Form {
1181    #[getter]
1182    fn name(&self) -> PyResult<String> {
1183        Ok(self.name.clone())
1184    }
1185
1186    #[getter]
1187    fn last_modified<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
1188        to_py_datetime_option(py, &self.last_modified)
1189    }
1190
1191    #[getter]
1192    fn who_last_modified_name(&self) -> PyResult<Option<String>> {
1193        Ok(self.who_last_modified_name.clone())
1194    }
1195
1196    #[getter]
1197    fn who_last_modified_role(&self) -> PyResult<Option<String>> {
1198        Ok(self.who_last_modified_role.clone())
1199    }
1200
1201    #[getter]
1202    fn when_created(&self) -> PyResult<usize> {
1203        Ok(self.when_created)
1204    }
1205
1206    #[getter]
1207    fn has_errors(&self) -> PyResult<bool> {
1208        Ok(self.has_errors)
1209    }
1210
1211    #[getter]
1212    fn has_warnings(&self) -> PyResult<bool> {
1213        Ok(self.has_warnings)
1214    }
1215
1216    #[getter]
1217    fn locked(&self) -> PyResult<bool> {
1218        Ok(self.locked)
1219    }
1220
1221    #[getter]
1222    fn user(&self) -> PyResult<Option<String>> {
1223        Ok(self.user.clone())
1224    }
1225
1226    #[getter]
1227    fn date_time_changed<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
1228        to_py_datetime_option(py, &self.date_time_changed)
1229    }
1230
1231    #[getter]
1232    fn form_title(&self) -> PyResult<String> {
1233        Ok(self.form_title.clone())
1234    }
1235
1236    #[getter]
1237    fn form_index(&self) -> PyResult<usize> {
1238        Ok(self.form_index)
1239    }
1240
1241    #[getter]
1242    fn form_group(&self) -> PyResult<Option<String>> {
1243        Ok(self.form_group.clone())
1244    }
1245
1246    #[getter]
1247    fn form_state(&self) -> PyResult<String> {
1248        Ok(self.form_state.clone())
1249    }
1250
1251    #[getter]
1252    fn states(&self) -> PyResult<Option<Vec<State>>> {
1253        Ok(self.states.clone())
1254    }
1255
1256    #[getter]
1257    fn lock_state(&self) -> PyResult<Option<LockState>> {
1258        Ok(self.lock_state.clone())
1259    }
1260
1261    #[getter]
1262    fn categories(&self) -> PyResult<Option<Vec<Category>>> {
1263        Ok(self.categories.clone())
1264    }
1265
1266    pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
1267        let dict = PyDict::new(py);
1268        dict.set_item("name", &self.name)?;
1269        dict.set_item(
1270            "last_modified",
1271            to_py_datetime_option(py, &self.last_modified)?,
1272        )?;
1273        dict.set_item("who_last_modified_name", &self.who_last_modified_name)?;
1274        dict.set_item("who_last_modified_role", &self.who_last_modified_role)?;
1275        dict.set_item("when_created", self.when_created)?;
1276        dict.set_item("has_errors", self.has_errors)?;
1277        dict.set_item("has_warnings", self.has_warnings)?;
1278        dict.set_item("locked", self.locked)?;
1279        dict.set_item("user", &self.user)?;
1280        dict.set_item(
1281            "date_time_changed",
1282            to_py_datetime_option(py, &self.date_time_changed)?,
1283        )?;
1284        dict.set_item("form_title", &self.form_title)?;
1285        dict.set_item("form_index", self.form_index)?;
1286        dict.set_item("form_group", &self.form_group)?;
1287        dict.set_item("form_state", &self.form_state)?;
1288
1289        let mut state_dicts = Vec::new();
1290        if let Some(states) = &self.states {
1291            for state in states {
1292                let state_dict = state.to_dict(py)?;
1293                state_dicts.push(state_dict);
1294            }
1295            dict.set_item("states", state_dicts)?;
1296        } else {
1297            dict.set_item("states", py.None())?;
1298        }
1299
1300        if let Some(lock_state) = &self.lock_state {
1301            dict.set_item("lock_state", lock_state.to_dict(py)?)?;
1302        } else {
1303            dict.set_item("lock_state", py.None())?;
1304        }
1305
1306        if let Some(categories) = &self.categories {
1307            let mut category_dicts = Vec::new();
1308            for category in categories {
1309                let category_dict = category.to_dict(py)?;
1310                category_dicts.push(category_dict);
1311            }
1312            dict.set_item("categories", category_dicts)?;
1313        } else {
1314            dict.set_item("categories", py.None())?;
1315        }
1316
1317        Ok(dict)
1318    }
1319}
1320
1321impl State {
1322    pub fn from_attributes(
1323        attrs: std::collections::HashMap<String, String>,
1324    ) -> Result<Self, crate::errors::Error> {
1325        let value = attrs.get("value").cloned().unwrap_or_default();
1326        let signer = attrs.get("signer").cloned().unwrap_or_default();
1327        let signer_unique_id = attrs.get("signerUniqueId").cloned().unwrap_or_default();
1328
1329        let date_signed = if let Some(ds) = attrs.get("dateSigned") {
1330            if ds.is_empty() {
1331                None
1332            } else {
1333                parse_datetime_internal(ds).ok()
1334            }
1335        } else {
1336            None
1337        };
1338
1339        Ok(State {
1340            value,
1341            signer,
1342            signer_unique_id,
1343            date_signed,
1344        })
1345    }
1346}
1347
1348impl LockState {
1349    pub fn from_attributes(
1350        attrs: std::collections::HashMap<String, String>,
1351    ) -> Result<Self, crate::errors::Error> {
1352        let locked = attrs.get("locked").map(|s| s == "true").unwrap_or(false);
1353        let user = attrs.get("user").filter(|s| !s.is_empty()).cloned();
1354        let user_unique_id = attrs.get("userUniqueId").filter(|s| !s.is_empty()).cloned();
1355
1356        let date_time_changed = if let Some(dtc) = attrs.get("dateTimeChanged") {
1357            if dtc.is_empty() {
1358                None
1359            } else {
1360                parse_datetime_internal(dtc).ok()
1361            }
1362        } else {
1363            None
1364        };
1365
1366        Ok(LockState {
1367            locked,
1368            user,
1369            user_unique_id,
1370            date_time_changed,
1371        })
1372    }
1373}
1374
1375impl Category {
1376    pub fn from_attributes(
1377        attrs: std::collections::HashMap<String, String>,
1378    ) -> Result<Self, crate::errors::Error> {
1379        let name = attrs.get("name").cloned().unwrap_or_default();
1380        let category_type = attrs.get("type").cloned().unwrap_or_default();
1381        let highest_index = attrs
1382            .get("highestIndex")
1383            .and_then(|s| s.parse().ok())
1384            .unwrap_or(0);
1385
1386        Ok(Category {
1387            name,
1388            category_type,
1389            highest_index,
1390            fields: None,
1391        })
1392    }
1393}
1394
1395impl Field {
1396    pub fn from_attributes(
1397        attrs: std::collections::HashMap<String, String>,
1398    ) -> Result<Self, crate::errors::Error> {
1399        let name = attrs.get("name").cloned().unwrap_or_default();
1400        let field_type = attrs.get("type").cloned().unwrap_or_default();
1401        let data_type = attrs.get("dataType").filter(|s| !s.is_empty()).cloned();
1402        let error_code = attrs.get("errorCode").cloned().unwrap_or_default();
1403
1404        let when_created = if let Some(wc) = attrs.get("whenCreated") {
1405            if wc.is_empty() {
1406                None
1407            } else {
1408                Some(parse_datetime_internal(wc)?)
1409            }
1410        } else {
1411            None
1412        };
1413
1414        let keep_history = attrs
1415            .get("keepHistory")
1416            .map(|s| s == "true")
1417            .unwrap_or(false);
1418
1419        Ok(Field {
1420            name,
1421            field_type,
1422            data_type,
1423            error_code,
1424            when_created,
1425            keep_history,
1426            entries: None,
1427            comments: None,
1428        })
1429    }
1430}
1431
1432impl Entry {
1433    pub fn from_attributes(
1434        attrs: std::collections::HashMap<String, String>,
1435    ) -> Result<Self, crate::errors::Error> {
1436        let entry_id = attrs
1437            .get("id")
1438            .or_else(|| attrs.get("entryId"))
1439            .cloned()
1440            .unwrap_or_default();
1441
1442        let reviewed_by = attrs.get("reviewedBy").filter(|s| !s.is_empty()).cloned();
1443        let reviewed_by_unique_id = attrs
1444            .get("reviewedByUniqueId")
1445            .filter(|s| !s.is_empty())
1446            .cloned();
1447
1448        let reviewed_by_when = if let Some(rbw) = attrs.get("reviewedByWhen") {
1449            if rbw.is_empty() {
1450                None
1451            } else {
1452                parse_datetime_internal(rbw).ok()
1453            }
1454        } else {
1455            None
1456        };
1457
1458        Ok(Entry {
1459            entry_id,
1460            reviewed_by,
1461            reviewed_by_unique_id,
1462            reviewed_by_when,
1463            value: None,
1464            reason: None,
1465        })
1466    }
1467}
1468
1469impl Value {
1470    pub fn from_attributes(
1471        attrs: std::collections::HashMap<String, String>,
1472    ) -> Result<Self, crate::errors::Error> {
1473        let by = attrs.get("by").cloned().unwrap_or_default();
1474        let by_unique_id = attrs.get("byUniqueId").filter(|s| !s.is_empty()).cloned();
1475        let role = attrs.get("role").cloned().unwrap_or_default();
1476
1477        let when = if let Some(w) = attrs.get("when") {
1478            if w.is_empty() {
1479                None
1480            } else {
1481                Some(parse_datetime_internal(w)?)
1482            }
1483        } else {
1484            None
1485        };
1486
1487        Ok(Value {
1488            by,
1489            by_unique_id,
1490            role,
1491            when,
1492            value: String::new(),
1493        })
1494    }
1495}
1496
1497impl Reason {
1498    pub fn from_attributes(
1499        attrs: std::collections::HashMap<String, String>,
1500    ) -> Result<Self, crate::errors::Error> {
1501        let by = attrs.get("by").cloned().unwrap_or_default();
1502        let by_unique_id = attrs.get("byUniqueId").filter(|s| !s.is_empty()).cloned();
1503        let role = attrs.get("role").cloned().unwrap_or_default();
1504
1505        let when = if let Some(w) = attrs.get("when") {
1506            if w.is_empty() {
1507                None
1508            } else {
1509                Some(parse_datetime_internal(w)?)
1510            }
1511        } else {
1512            None
1513        };
1514
1515        Ok(Reason {
1516            by,
1517            by_unique_id,
1518            role,
1519            when,
1520            value: String::new(),
1521        })
1522    }
1523}