Skip to main content

prelude_xml_parser/
lib.rs

1pub mod errors;
2pub mod native;
3
4use std::{borrow::Cow, collections::HashMap, fs::read_to_string, path::Path, str::from_utf8};
5
6use rayon::prelude::*;
7
8use crate::errors::Error;
9use crate::native::{
10    common::{Category, Comment, Entry, Field, LockState, Reason, State, Value},
11    site_native::{Site, SiteNative},
12    subject_native::{Form, Patient, SubjectNative},
13    user_native::{User, UserNative},
14};
15use quick_xml::events::{BytesStart, Event};
16use quick_xml::Reader;
17
18/// Parses a Prelude native XML file into a `Native` struct.
19///
20/// # Example
21///
22/// ```
23/// use std::path::Path;
24///
25/// use prelude_xml_parser::parse_site_native_file;
26///
27/// let file_path = Path::new("tests/assets/site_native.xml");
28/// let native = parse_site_native_file(&file_path).unwrap();
29///
30/// assert!(native.sites.len() >= 1, "Vector length is less than 1");
31/// ```
32pub fn parse_site_native_file(xml_path: &Path) -> Result<SiteNative, Error> {
33    check_valid_xml_file(xml_path)?;
34
35    let xml_file = read_to_string(xml_path)?;
36    let native = parse_site_native_string(&xml_file)?;
37
38    Ok(native)
39}
40
41/// Parse a string of Prelude native site XML into a `SiteNative` struct.
42///
43/// # Example
44///
45/// ```
46/// use chrono::{DateTime, Utc};
47/// use prelude_xml_parser::parse_site_native_string;
48/// use prelude_xml_parser::native::site_native::*;
49///
50/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
51/// <export_from_vision_EDC date="01-Jun-2024 18:17 -0500" createdBy="Paul Sanders" role="Project Manager" numberSubjectsProcessed="2">
52///
53///   <site name="Some Site" uniqueId="1681574834910" numberOfPatients="4" countOfRandomizedPatients="0" whenCreated="2023-04-15 12:08:19 -0400" creator="Paul Sanders" numberOfForms="1">
54///     <form name="demographic.form.name.site.demographics" lastModified="2023-04-15 12:08:19 -0400" whoLastModifiedName="Paul Sanders" whoLastModifiedRole="Project Manager" whenCreated="1681574834930" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Site Demographics" formIndex="1" formGroup="Demographic" formState="In-Work">
55///       <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-04-15 12:08:19 -0400" />
56///       <category name="Demographics" type="normal" highestIndex="0">
57///         <field name="address" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true" />
58///         <field name="company" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true">
59///           <entry id="1">
60///             <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-04-15 12:08:19 -0400" xml:space="preserve">Some Company</value>
61///           </entry>
62///         </field>
63///         <field name="site_code_name" type="hidden" dataType="string" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true">
64///           <entry id="1">
65///             <value by="set from calculation" byUniqueId="" role="System" when="2023-04-15 12:08:19 -0400" xml:space="preserve">ABC-Some Site</value>
66///             <reason by="set from calculation" byUniqueId="" role="System" when="2023-04-15 12:08:19 -0400" xml:space="preserve">calculated value</reason>
67///           </entry>
68///           <entry id="2">
69///             <value by="set from calculation" byUniqueId="" role="System" when="2023-04-15 12:07:24 -0400" xml:space="preserve">Some Site</value>
70///             <reason by="set from calculation" byUniqueId="" role="System" when="2023-04-15 12:07:24 -0400" xml:space="preserve">calculated value</reason>
71///           </entry>
72///         </field>
73///       </category>
74///       <category name="Enrollment" type="normal" highestIndex="0">
75///         <field name="enrollment_closed_date" type="popUpCalendar" dataType="date" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true" />
76///         <field name="enrollment_open" type="radio" dataType="string" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true">
77///           <entry id="1">
78///             <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-04-15 12:08:19 -0400" xml:space="preserve">Yes</value>
79///           </entry>
80///         </field>
81///         <field name="enrollment_open_date" type="popUpCalendar" dataType="date" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true" />
82///       </category>
83///     </form>
84///   </site>
85///
86///   <site name="Artemis" uniqueId="1691420994591" numberOfPatients="0" countOfRandomizedPatients="0" whenCreated="2023-08-07 08:14:23 -0700" creator="Paul Sanders" numberOfForms="1">
87///     <form name="demographic.form.name.site.demographics" lastModified="2023-08-07 08:14:23 -0700" whoLastModifiedName="Paul Sanders" whoLastModifiedRole="Project Manager" whenCreated="1691420994611" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Site Demographics" formIndex="1" formGroup="Demographic" formState="In-Work">
88///       <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-08-07 08:14:23 -0700" />
89///       <category name="Demographics" type="normal" highestIndex="0">
90///         <field name="address" type="text" dataType="string" errorCode="valid" whenCreated="2023-08-07 10:09:54 -0500" keepHistory="true">
91///           <entry id="1">
92///             <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-08-07 08:14:21 -0700" xml:space="preserve">1111 Moon Drive</value>
93///           </entry>
94///           <comment id="1">
95///             <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-08-07 08:14:21 -0700" xml:space="preserve">Some comment</value>
96///           </comment>
97///         </field>
98///       </category>
99///     </form>
100///   </site>
101///
102/// </export_from_vision_EDC>
103/// "#;
104///
105/// let expected = SiteNative {
106///     sites: vec![
107///         Site {
108///             name: "Some Site".to_string(),
109///             unique_id: "1681574834910".to_string(),
110///             number_of_patients: 4,
111///             count_of_randomized_patients: 0,
112///             when_created: Some(DateTime::parse_from_rfc3339("2023-04-15T16:08:19Z")
113///                 .unwrap()
114///                 .with_timezone(&Utc)),
115///             creator: "Paul Sanders".to_string(),
116///             number_of_forms: 1,
117///             forms: Some(vec![Form {
118///                 name: "demographic.form.name.site.demographics".to_string(),
119///                 last_modified: Some(
120///                     DateTime::parse_from_rfc3339("2023-04-15T16:08:19Z")
121///                         .unwrap()
122///                         .with_timezone(&Utc),
123///                 ),
124///                 who_last_modified_name: Some("Paul Sanders".to_string()),
125///                 who_last_modified_role: Some("Project Manager".to_string()),
126///                 when_created: 1681574834930,
127///                 has_errors: false,
128///                 has_warnings: false,
129///                 locked: false,
130///                 user: None,
131///                 date_time_changed: None,
132///                 form_title: "Site Demographics".to_string(),
133///                 form_index: 1,
134///                 form_group: Some("Demographic".to_string()),
135///                 form_state: "In-Work".to_string(),
136///                 lock_state: None,
137///                 states: Some(vec![State {
138///                     value: "form.state.in.work".to_string(),
139///                     signer: "Paul Sanders - Project Manager".to_string(),
140///                     signer_unique_id: "1681162687395".to_string(),
141///                     date_signed: Some(
142///                         DateTime::parse_from_rfc3339("2023-04-15T16:08:19Z")
143///                             .unwrap()
144///                             .with_timezone(&Utc),
145///                     ),
146///                 }]),
147///                 categories: Some(vec![
148///                     Category {
149///                         name: "Demographics".to_string(),
150///                         category_type: "normal".to_string(),
151///                         highest_index: 0,
152///                         fields: Some(vec![
153///                             Field {
154///                                 name: "address".to_string(),
155///                                 field_type: "text".to_string(),
156///                                 data_type: Some("string".to_string()),
157///                                 error_code: "valid".to_string(),
158///                                 when_created: Some(DateTime::parse_from_rfc3339(
159///                                     "2023-04-15T16:07:14Z",
160///                                 )
161///                                 .unwrap()
162///                                 .with_timezone(&Utc)),
163///                                 keep_history: true,
164///                                 entries: None,
165///                                 comments: None,
166///                             },
167///                             Field {
168///                                 name: "company".to_string(),
169///                                 field_type: "text".to_string(),
170///                                 data_type: Some("string".to_string()),
171///                                 error_code: "valid".to_string(),
172///                                 when_created: Some(DateTime::parse_from_rfc3339(
173///                                     "2023-04-15T16:07:14Z",
174///                                 )
175///                                 .unwrap()
176///                                 .with_timezone(&Utc)),
177///                                 keep_history: true,
178///                                 entries: Some(vec![Entry {
179///                                     entry_id: "1".to_string(),
180///                                     reviewed_by: None,
181///                                     reviewed_by_unique_id: None,
182///                                     reviewed_by_when: None,
183///                                     value: Some(Value {
184///                                         by: "Paul Sanders".to_string(),
185///                                         by_unique_id: Some("1681162687395".to_string()),
186///                                         role: "Project Manager".to_string(),
187///                                         when: Some(DateTime::parse_from_rfc3339(
188///                                             "2023-04-15T16:08:19Z",
189///                                         )
190///                                         .unwrap()
191///                                         .with_timezone(&Utc)),
192///                                         value: "Some Company".to_string(),
193///                                     }),
194///                                     reason: None,
195///                                 }]),
196///                                 comments: None,
197///                             },
198///                             Field {
199///                                 name: "site_code_name".to_string(),
200///                                 field_type: "hidden".to_string(),
201///                                 data_type: Some("string".to_string()),
202///                                 error_code: "valid".to_string(),
203///                                 when_created: Some(DateTime::parse_from_rfc3339(
204///                                     "2023-04-15T16:07:14Z",
205///                                 )
206///                                 .unwrap()
207///                                 .with_timezone(&Utc)),
208///                                 keep_history: true,
209///                                 entries: Some(vec![
210///                                     Entry {
211///                                         entry_id: "1".to_string(),
212///                                         reviewed_by: None,
213///                                         reviewed_by_unique_id: None,
214///                                         reviewed_by_when: None,
215///                                         value: Some(Value {
216///                                             by: "set from calculation".to_string(),
217///                                             by_unique_id: None,
218///                                             role: "System".to_string(),
219///                                             when: Some(DateTime::parse_from_rfc3339(
220///                                                 "2023-04-15T16:08:19Z",
221///                                             )
222///                                             .unwrap()
223///                                             .with_timezone(&Utc)),
224///                                             value: "ABC-Some Site".to_string(),
225///                                         }),
226///                                         reason: Some(Reason {
227///                                             by: "set from calculation".to_string(),
228///                                             by_unique_id: None,
229///                                             role: "System".to_string(),
230///                                             when: Some(DateTime::parse_from_rfc3339(
231///                                                 "2023-04-15T16:08:19Z",
232///                                             )
233///                                             .unwrap()
234///                                             .with_timezone(&Utc)),
235///                                             value: "calculated value".to_string(),
236///                                         }),
237///                                     },
238///                                     Entry {
239///                                         entry_id: "2".to_string(),
240///                                         reviewed_by: None,
241///                                         reviewed_by_unique_id: None,
242///                                         reviewed_by_when: None,
243///                                         value: Some(Value {
244///                                             by: "set from calculation".to_string(),
245///                                             by_unique_id: None,
246///                                             role: "System".to_string(),
247///                                             when: Some(DateTime::parse_from_rfc3339(
248///                                                 "2023-04-15T16:07:24Z",
249///                                             )
250///                                             .unwrap()
251///                                             .with_timezone(&Utc)),
252///                                             value: "Some Site".to_string(),
253///                                         }),
254///                                         reason: Some(Reason {
255///                                             by: "set from calculation".to_string(),
256///                                             by_unique_id: None,
257///                                             role: "System".to_string(),
258///                                             when: Some(DateTime::parse_from_rfc3339(
259///                                                 "2023-04-15T16:07:24Z",
260///                                             )
261///                                             .unwrap()
262///                                             .with_timezone(&Utc)),
263///                                             value: "calculated value".to_string(),
264///                                         }),
265///                                     },
266///                                 ]),
267///                                 comments: None,
268///                             },
269///                         ]),
270///                     },
271///                     Category {
272///                         name: "Enrollment".to_string(),
273///                         category_type: "normal".to_string(),
274///                         highest_index: 0,
275///                         fields: Some(vec![
276///                             Field {
277///                                 name: "enrollment_closed_date".to_string(),
278///                                 field_type: "popUpCalendar".to_string(),
279///                                 data_type: Some("date".to_string()),
280///                                 error_code: "valid".to_string(),
281///                                 when_created: Some(DateTime::parse_from_rfc3339(
282///                                     "2023-04-15T16:07:14Z",
283///                                 )
284///                                 .unwrap()
285///                                 .with_timezone(&Utc)),
286///                                 keep_history: true,
287///                                 entries: None,
288///                                 comments: None,
289///                             },
290///                             Field {
291///                                 name: "enrollment_open".to_string(),
292///                                 field_type: "radio".to_string(),
293///                                 data_type: Some("string".to_string()),
294///                                 error_code: "valid".to_string(),
295///                                 when_created: Some(DateTime::parse_from_rfc3339(
296///                                     "2023-04-15T16:07:14Z",
297///                                 )
298///                                 .unwrap()
299///                                 .with_timezone(&Utc)),
300///                                 keep_history: true,
301///                                 entries: Some(vec![Entry {
302///                                     entry_id: "1".to_string(),
303///                                     reviewed_by: None,
304///                                     reviewed_by_unique_id: None,
305///                                     reviewed_by_when: None,
306///                                     value: Some(Value {
307///                                         by: "Paul Sanders".to_string(),
308///                                         by_unique_id: Some("1681162687395".to_string()),
309///                                         role: "Project Manager".to_string(),
310///                                         when: Some(DateTime::parse_from_rfc3339(
311///                                             "2023-04-15T16:08:19Z",
312///                                         )
313///                                         .unwrap()
314///                                         .with_timezone(&Utc)),
315///                                         value: "Yes".to_string(),
316///                                     }),
317///                                     reason: None,
318///                                 }]),
319///                                 comments: None,
320///                             },
321///                             Field {
322///                                 name: "enrollment_open_date".to_string(),
323///                                 field_type: "popUpCalendar".to_string(),
324///                                 data_type: Some("date".to_string()),
325///                                 error_code: "valid".to_string(),
326///                                 when_created: Some(DateTime::parse_from_rfc3339(
327///                                     "2023-04-15T16:07:14Z",
328///                                 )
329///                                 .unwrap()
330///                                 .with_timezone(&Utc)),
331///                                 keep_history: true,
332///                                 entries: None,
333///                                 comments: None,
334///                             },
335///                         ]),
336///                     },
337///                 ]),
338///             }]),
339///         },
340///         Site {
341///             name: "Artemis".to_string(),
342///             unique_id: "1691420994591".to_string(),
343///             number_of_patients: 0,
344///             count_of_randomized_patients: 0,
345///             when_created: Some(DateTime::parse_from_rfc3339("2023-08-07T15:14:23Z")
346///                 .unwrap()
347///                 .with_timezone(&Utc)),
348///             creator: "Paul Sanders".to_string(),
349///             number_of_forms: 1,
350///             forms: Some(vec![Form {
351///                 name: "demographic.form.name.site.demographics".to_string(),
352///                 last_modified: Some(
353///                     DateTime::parse_from_rfc3339("2023-08-07T15:14:23Z")
354///                         .unwrap()
355///                         .with_timezone(&Utc),
356///                 ),
357///                 who_last_modified_name: Some("Paul Sanders".to_string()),
358///                 who_last_modified_role: Some("Project Manager".to_string()),
359///                 when_created: 1691420994611,
360///                 has_errors: false,
361///                 has_warnings: false,
362///                 locked: false,
363///                 user: None,
364///                 date_time_changed: None,
365///                 form_title: "Site Demographics".to_string(),
366///                 form_index: 1,
367///                 form_group: Some("Demographic".to_string()),
368///                 form_state: "In-Work".to_string(),
369///                 lock_state: None,
370///                 states: Some(vec![State {
371///                     value: "form.state.in.work".to_string(),
372///                     signer: "Paul Sanders - Project Manager".to_string(),
373///                     signer_unique_id: "1681162687395".to_string(),
374///                     date_signed: Some(
375///                         DateTime::parse_from_rfc3339("2023-08-07T15:14:23Z")
376///                             .unwrap()
377///                             .with_timezone(&Utc),
378///                     ),
379///                 }]),
380///                 categories: Some(vec![Category {
381///                     name: "Demographics".to_string(),
382///                     category_type: "normal".to_string(),
383///                     highest_index: 0,
384///                     fields: Some(vec![Field {
385///                         name: "address".to_string(),
386///                         field_type: "text".to_string(),
387///                         data_type: Some("string".to_string()),
388///                         error_code: "valid".to_string(),
389///                         when_created: Some(DateTime::parse_from_rfc3339("2023-08-07T15:09:54Z")
390///                             .unwrap()
391///                             .with_timezone(&Utc)),
392///                         keep_history: true,
393///                         entries: Some(vec![Entry {
394///                             entry_id: "1".to_string(),
395///                             reviewed_by: None,
396///                             reviewed_by_unique_id: None,
397///                             reviewed_by_when: None,
398///                             value: Some(Value {
399///                                 by: "Paul Sanders".to_string(),
400///                                 by_unique_id: Some("1681162687395".to_string()),
401///                                 role: "Project Manager".to_string(),
402///                                 when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:14:21Z")
403///                                     .unwrap()
404///                                     .with_timezone(&Utc)),
405///                                 value: "1111 Moon Drive".to_string(),
406///                             }),
407///                             reason: None,
408///                         }]),
409///                         comments: Some(vec![Comment {
410///                             comment_id: "1".to_string(),
411///                             value: Some(Value {
412///                                 by: "Paul Sanders".to_string(),
413///                                 by_unique_id: Some("1681162687395".to_string()),
414///                                 role: "Project Manager".to_string(),
415///                                 when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:14:21Z")
416///                                     .unwrap()
417///                                     .with_timezone(&Utc)),
418///                                 value: "Some comment".to_string(),
419///                             }),
420///                         }]),
421///                     }]),
422///                 }]),
423///             }]),
424///         },
425///     ],
426/// };
427/// let result = parse_site_native_string(xml).unwrap();
428/// assert_eq!(result, expected);
429pub fn parse_site_native_string(xml_str: &str) -> Result<SiteNative, Error> {
430    let chunks = extract_site_chunks(xml_str);
431    let sites = chunks
432        .into_par_iter()
433        .map(parse_site_xml)
434        .collect::<Result<Vec<_>, _>>()?;
435    Ok(SiteNative { sites })
436}
437
438/// Parses a Prelude native subject XML file into a `SubjectNative` struct.
439///
440/// # Example
441///
442/// ```
443/// use std::path::Path;
444///
445/// use prelude_xml_parser::parse_subject_native_file;
446///
447/// let file_path = Path::new("tests/assets/subject_native.xml");
448/// let native = parse_subject_native_file(&file_path).unwrap();
449///
450/// assert!(native.patients.len() >= 1, "Vector length is less than 1");
451/// ```
452pub fn parse_subject_native_file(xml_path: &Path) -> Result<SubjectNative, Error> {
453    check_valid_xml_file(xml_path)?;
454
455    let xml_str = read_to_string(xml_path)?;
456    let chunks = extract_patient_chunks(&xml_str);
457    let patients = chunks
458        .into_par_iter()
459        .map(parse_patient_xml)
460        .collect::<Result<Vec<_>, _>>()?;
461    Ok(SubjectNative { patients })
462}
463
464/// Parse a string of Prelude native subject XML into a `SubjectNative` struct.
465///
466/// # Example
467///
468/// ```
469/// use chrono::{DateTime, Utc};
470/// use prelude_xml_parser::parse_subject_native_string;
471/// use prelude_xml_parser::native::common::LockState;
472/// use prelude_xml_parser::native::subject_native::*;
473///
474/// let xml = r#"<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Paul Sanders" role="Project Manager" numberSubjectsProcessed="4">
475///     <patient patientId="ABC-001" uniqueId="1681574905819" whenCreated="2023-04-15 12:09:02 -0400" creator="Paul Sanders" siteName="Some Site" siteUniqueId="1681574834910" lastLanguage="English" numberOfForms="6">
476///       <form name="day.0.form.name.demographics" lastModified="2023-04-15 12:09:15 -0400" whoLastModifiedName="Paul Sanders" whoLastModifiedRole="Project Manager" whenCreated="1681574905839" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Demographics" formIndex="1" formGroup="Day 0" formState="In-Work">
477///         <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-04-15 12:09:02 -0400"/>
478///         <category name="Demographics" type="normal" highestIndex="0">
479///           <field name="breed" type="combo-box" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
480///             <entry id="1">
481///               <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Labrador</value>
482///             </entry>
483///           </field>
484///         </category>
485///       </form>
486///     </patient>
487///     <patient patientId="DEF-002" uniqueId="1681574905820" whenCreated="2023-04-16 12:10:02 -0400" creator="Wade Watts" siteName="Another Site" siteUniqueId="1681574834911" lastLanguage="" numberOfForms="8">
488///       <form name="day.0.form.name.demographics" lastModified="2023-04-16 12:10:15 -0400" whoLastModifiedName="Barney Rubble" whoLastModifiedRole="Technician" whenCreated="1681574905838" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Demographics" formIndex="1" formGroup="Day 0" formState="In-Work">
489///         <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-04-16 12:10:02 -0400"/>
490///         <category name="Demographics" type="normal" highestIndex="0">
491///           <field name="breed" type="combo-box" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
492///             <entry id="1">
493///               <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Labrador</value>
494///             </entry>
495///           </field>
496///         </category>
497///       </form>
498///     </patient>
499/// </export_from_vision_EDC>
500/// "#;
501///
502/// let expected = SubjectNative {
503///     patients: vec![
504///         Patient {
505///             patient_id: "ABC-001".to_string(),
506///             unique_id: "1681574905819".to_string(),
507///             when_created: Some(DateTime::parse_from_rfc3339("2023-04-15T16:09:02Z")
508///                 .unwrap()
509///                 .with_timezone(&Utc)),
510///             creator: "Paul Sanders".to_string(),
511///             site_name: "Some Site".to_string(),
512///             site_unique_id: "1681574834910".to_string(),
513///             last_language: Some("English".to_string()),
514///             number_of_forms: 6,
515///             forms: Some(vec![Form {
516///                 name: "day.0.form.name.demographics".to_string(),
517///                 last_modified: Some(DateTime::parse_from_rfc3339("2023-04-15T16:09:15Z")
518///                     .unwrap()
519///                     .with_timezone(&Utc)),
520///                 who_last_modified_name: Some("Paul Sanders".to_string()),
521///                 who_last_modified_role: Some("Project Manager".to_string()),
522///                 when_created: 1681574905839,
523///                 has_errors: false,
524///                 has_warnings: false,
525///                 locked: false,
526///                 user: None,
527///                 date_time_changed: None,
528///                 form_title: "Demographics".to_string(),
529///                 form_index: 1,
530///                 form_group: Some("Day 0".to_string()),
531///                 form_state: "In-Work".to_string(),
532///                 lock_state: None,
533///                 states: Some(vec![State {
534///                     value: "form.state.in.work".to_string(),
535///                     signer: "Paul Sanders - Project Manager".to_string(),
536///                     signer_unique_id: "1681162687395".to_string(),
537///                     date_signed: Some(
538///                         DateTime::parse_from_rfc3339("2023-04-15T16:09:02Z")
539///                             .unwrap()
540///                             .with_timezone(&Utc),
541///                     ),
542///                 }]),
543///                 categories: Some(vec![Category {
544///                     name: "Demographics".to_string(),
545///                     category_type: "normal".to_string(),
546///                     highest_index: 0,
547///                     fields: Some(vec![Field {
548///                         name: "breed".to_string(),
549///                         field_type: "combo-box".to_string(),
550///                         data_type: Some("string".to_string()),
551///                         error_code: "valid".to_string(),
552///                         when_created: Some(DateTime::parse_from_rfc3339("2023-04-15T16:08:26Z")
553///                             .unwrap()
554///                             .with_timezone(&Utc)),
555///                         keep_history: true,
556///                         entries: Some(vec![Entry {
557///                             entry_id: "1".to_string(),
558///                             reviewed_by: None,
559///                             reviewed_by_unique_id: None,
560///                             reviewed_by_when: None,
561///                             value: Some(Value {
562///                                 by: "Paul Sanders".to_string(),
563///                                 by_unique_id: Some("1681162687395".to_string()),
564///                                 role: "Project Manager".to_string(),
565///                                 when: Some(DateTime::parse_from_rfc3339("2023-04-15T16:09:02Z")
566///                                     .unwrap()
567///                                     .with_timezone(&Utc)),
568///                                 value: "Labrador".to_string(),
569///                             }),
570///                             reason: None,
571///                         }]),
572///                         comments: None,
573///                     }]),
574///                 }]),
575///             }]),
576///         },
577///         Patient {
578///             patient_id: "DEF-002".to_string(),
579///             unique_id: "1681574905820".to_string(),
580///             when_created: Some(DateTime::parse_from_rfc3339("2023-04-16T16:10:02Z")
581///                 .unwrap()
582///                 .with_timezone(&Utc)),
583///             creator: "Wade Watts".to_string(),
584///             site_name: "Another Site".to_string(),
585///             site_unique_id: "1681574834911".to_string(),
586///             last_language: None,
587///             number_of_forms: 8,
588///             forms: Some(vec![Form {
589///                 name: "day.0.form.name.demographics".to_string(),
590///                 last_modified: Some(DateTime::parse_from_rfc3339("2023-04-16T16:10:15Z")
591///                     .unwrap()
592///                     .with_timezone(&Utc)),
593///                 who_last_modified_name: Some("Barney Rubble".to_string()),
594///                 who_last_modified_role: Some("Technician".to_string()),
595///                 when_created: 1681574905838,
596///                 has_errors: false,
597///                 has_warnings: false,
598///                 locked: false,
599///                 user: None,
600///                 date_time_changed: None,
601///                 form_title: "Demographics".to_string(),
602///                 form_index: 1,
603///                 form_group: Some("Day 0".to_string()),
604///                 form_state: "In-Work".to_string(),
605///                 lock_state: None,
606///                 states: Some(vec![State {
607///                     value: "form.state.in.work".to_string(),
608///                     signer: "Paul Sanders - Project Manager".to_string(),
609///                     signer_unique_id: "1681162687395".to_string(),
610///                     date_signed: Some(
611///                         DateTime::parse_from_rfc3339("2023-04-16T16:10:02Z")
612///                             .unwrap()
613///                             .with_timezone(&Utc),
614///                     ),
615///                 }]),
616///                 categories: Some(vec![Category {
617///                     name: "Demographics".to_string(),
618///                     category_type: "normal".to_string(),
619///                     highest_index: 0,
620///                     fields: Some(vec![Field {
621///                         name: "breed".to_string(),
622///                         field_type: "combo-box".to_string(),
623///                         data_type: Some("string".to_string()),
624///                         error_code: "valid".to_string(),
625///                         when_created: Some(DateTime::parse_from_rfc3339("2023-04-15T16:08:26Z")
626///                             .unwrap()
627///                             .with_timezone(&Utc)),
628///                         keep_history: true,
629///                         entries: Some(vec![Entry {
630///                             entry_id: "1".to_string(),
631///                             reviewed_by: None,
632///                             reviewed_by_unique_id: None,
633///                             reviewed_by_when: None,
634///                             value: Some(Value {
635///                                 by: "Paul Sanders".to_string(),
636///                                 by_unique_id: Some("1681162687395".to_string()),
637///                                 role: "Project Manager".to_string(),
638///                                 when: Some(DateTime::parse_from_rfc3339("2023-04-15T16:09:02Z")
639///                                     .unwrap()
640///                                     .with_timezone(&Utc)),
641///                                 value: "Labrador".to_string(),
642///                             }),
643///                             reason: None,
644///                         }]),
645///                         comments: None,
646///                     }]),
647///                 }]),
648///             }]),
649///         },
650///     ],
651/// };
652/// let result = parse_subject_native_string(xml).unwrap();
653///
654/// assert_eq!(result, expected);
655/// ```
656pub fn parse_subject_native_string(xml_str: &str) -> Result<SubjectNative, Error> {
657    let chunks = extract_patient_chunks(xml_str);
658    let patients = chunks
659        .into_par_iter()
660        .map(parse_patient_xml)
661        .collect::<Result<Vec<_>, _>>()?;
662    Ok(SubjectNative { patients })
663}
664
665fn extract_attributes<'a>(e: &'a BytesStart<'a>) -> Result<HashMap<&'a str, &'a str>, Error> {
666    let mut attrs = HashMap::new();
667    for attr in e.attributes() {
668        let attr = attr.map_err(|e| {
669            Error::ParsingError(quick_xml::de::DeError::Custom(format!(
670                "Attribute error: {}",
671                e
672            )))
673        })?;
674        let Cow::Borrowed(value) = attr.value else {
675            return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
676                "Attribute value was not borrowed from the source".to_string(),
677            )));
678        };
679        let (Ok(key), Ok(value)) = (from_utf8(attr.key.into_inner()), from_utf8(value)) else {
680            return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
681                "Attribute was not valid UTF-8".to_string(),
682            )));
683        };
684        attrs.insert(key, value);
685    }
686    Ok(attrs)
687}
688
689fn extract_patient_chunks(xml: &str) -> Vec<&str> {
690    let mut chunks = Vec::new();
691    let mut pos = 0;
692    loop {
693        match xml[pos..].find("<patient ") {
694            None => break,
695            Some(rel) => {
696                let start = pos + rel;
697                match xml[start..].find("</patient>") {
698                    None => break,
699                    Some(rel_end) => {
700                        let end = start + rel_end + "</patient>".len();
701                        chunks.push(&xml[start..end]);
702                        pos = end;
703                    }
704                }
705            }
706        }
707    }
708    chunks
709}
710
711fn parse_patient_xml(patient_xml: &str) -> Result<Patient, Error> {
712    let mut xml_reader = Reader::from_str(patient_xml);
713    xml_reader.config_mut().trim_text(true);
714
715    let mut current_patient: Option<Patient> = None;
716    let mut current_forms: Vec<Form> = Vec::new();
717    let mut current_form: Option<Form> = None;
718    let mut current_states: Vec<State> = Vec::new();
719    let mut current_categories: Vec<Category> = Vec::new();
720    let mut current_category: Option<Category> = None;
721    let mut current_fields: Vec<Field> = Vec::new();
722    let mut current_field: Option<Field> = None;
723    let mut current_entries: Vec<Entry> = Vec::new();
724    let mut current_entry: Option<Entry> = None;
725    let mut current_comments: Vec<Comment> = Vec::new();
726    let mut current_comment: Option<Comment> = None;
727    let mut current_value: Option<Value> = None;
728    let mut current_reason: Option<Reason> = None;
729    let mut text_content = String::new();
730    let mut in_form = false;
731    let mut in_category = false;
732    let mut in_field = false;
733    let mut in_entry = false;
734    let mut in_comment = false;
735    let mut in_value = false;
736    let mut in_reason = false;
737
738    loop {
739        match xml_reader.read_event() {
740            Err(e) => {
741                return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
742                    format!("XML reading error: {}", e),
743                )))
744            }
745            Ok(Event::Eof) => break,
746
747            Ok(Event::Start(ref e)) => {
748                let name_bytes = e.local_name();
749                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
750                    match name {
751                        "patient" => {
752                            let attrs = extract_attributes(e)?;
753                            current_patient = Some(Patient::from_attributes(attrs)?);
754                            current_forms.clear();
755                        }
756                        "form" if current_patient.is_some() => {
757                            let attrs = extract_attributes(e)?;
758                            current_form = Some(Form::from_attributes(attrs)?);
759                            in_form = true;
760                            current_states.clear();
761                            current_categories.clear();
762                        }
763                        "category" if in_form => {
764                            let attrs = extract_attributes(e)?;
765                            current_category = Some(Category::from_attributes(attrs)?);
766                            in_category = true;
767                            current_fields.clear();
768                        }
769                        "field" if in_category => {
770                            let attrs = extract_attributes(e)?;
771                            current_field = Some(Field::from_attributes(attrs)?);
772                            in_field = true;
773                            current_entries.clear();
774                            current_comments.clear();
775                        }
776                        "entry" if in_field => {
777                            let attrs = extract_attributes(e)?;
778                            current_entry = Some(Entry::from_attributes(attrs)?);
779                            in_entry = true;
780                        }
781                        "comment" if in_field => {
782                            let attrs = extract_attributes(e)?;
783                            let comment_id = attrs.get("id").cloned().unwrap_or_default();
784                            current_comment = Some(Comment {
785                                comment_id: comment_id.to_string(),
786                                value: None,
787                            });
788                            in_comment = true;
789                        }
790                        "value" if in_entry || in_comment => {
791                            let attrs = extract_attributes(e)?;
792                            current_value = Some(Value::from_attributes(attrs)?);
793                            in_value = true;
794                            text_content.clear();
795                        }
796                        "reason" if in_entry => {
797                            let attrs = extract_attributes(e)?;
798                            current_reason = Some(Reason::from_attributes(attrs)?);
799                            in_reason = true;
800                            text_content.clear();
801                        }
802                        _ => {}
803                    }
804                }
805            }
806
807            Ok(Event::Text(e)) if (in_value || in_reason) => {
808                text_content.push_str(&String::from_utf8_lossy(&e));
809            }
810
811            Ok(Event::End(ref e)) => {
812                let name_bytes = e.local_name();
813                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
814                    match name {
815                        "patient" => {
816                            if let Some(mut patient) = current_patient.take() {
817                                if !current_forms.is_empty() {
818                                    patient.set_forms(std::mem::take(&mut current_forms));
819                                }
820                                current_patient = Some(patient);
821                            }
822                        }
823                        "form" if in_form => {
824                            if let Some(mut form) = current_form.take() {
825                                if !current_states.is_empty() {
826                                    form.states = Some(std::mem::take(&mut current_states));
827                                }
828                                if !current_categories.is_empty() {
829                                    form.categories = Some(std::mem::take(&mut current_categories));
830                                }
831                                current_forms.push(form);
832                            }
833                            in_form = false;
834                        }
835                        "category" if in_category => {
836                            if let Some(mut category) = current_category.take() {
837                                if !current_fields.is_empty() {
838                                    category.fields = Some(std::mem::take(&mut current_fields));
839                                }
840                                current_categories.push(category);
841                            }
842                            in_category = false;
843                        }
844                        "field" if in_field => {
845                            if let Some(mut field) = current_field.take() {
846                                if !current_entries.is_empty() {
847                                    field.entries = Some(std::mem::take(&mut current_entries));
848                                }
849                                if !current_comments.is_empty() {
850                                    field.comments = Some(std::mem::take(&mut current_comments));
851                                }
852                                current_fields.push(field);
853                            }
854                            in_field = false;
855                        }
856                        "entry" if in_entry => {
857                            if let Some(entry) = current_entry.take() {
858                                current_entries.push(entry);
859                            }
860                            in_entry = false;
861                        }
862                        "comment" if in_comment => {
863                            if let Some(comment) = current_comment.take() {
864                                current_comments.push(comment);
865                            }
866                            in_comment = false;
867                        }
868                        "value" if in_value => {
869                            if let Some(mut value) = current_value.take() {
870                                value.value = std::mem::take(&mut text_content);
871                                if let Some(ref mut entry) = current_entry {
872                                    entry.value = Some(value);
873                                } else if let Some(ref mut comment) = current_comment {
874                                    comment.value = Some(value);
875                                }
876                            }
877                            in_value = false;
878                        }
879                        "reason" if in_reason => {
880                            if let Some(mut reason) = current_reason.take() {
881                                reason.value = std::mem::take(&mut text_content);
882                                if let Some(ref mut entry) = current_entry {
883                                    entry.reason = Some(reason);
884                                }
885                            }
886                            in_reason = false;
887                        }
888                        _ => {}
889                    }
890                }
891            }
892
893            Ok(Event::Empty(ref e)) => {
894                let name_bytes = e.local_name();
895                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
896                    match name {
897                        "state" if in_form => {
898                            let attrs = extract_attributes(e)?;
899                            let state = State::from_attributes(attrs)?;
900                            current_states.push(state);
901                        }
902                        "lockState" if in_form => {
903                            let attrs = extract_attributes(e)?;
904                            let lock_state = LockState::from_attributes(attrs)?;
905                            if let Some(ref mut form) = current_form {
906                                form.lock_state = Some(lock_state);
907                            }
908                        }
909                        "value" if in_entry => {
910                            let attrs = extract_attributes(e)?;
911                            let value = Value::from_attributes(attrs)?;
912                            if let Some(ref mut entry) = current_entry {
913                                entry.value = Some(value);
914                            }
915                        }
916                        "reason" if in_entry => {
917                            let attrs = extract_attributes(e)?;
918                            let reason = Reason::from_attributes(attrs)?;
919                            if let Some(ref mut entry) = current_entry {
920                                entry.reason = Some(reason);
921                            }
922                        }
923                        _ => {}
924                    }
925                }
926            }
927
928            _ => {}
929        }
930    }
931
932    current_patient.ok_or_else(|| {
933        Error::ParsingError(quick_xml::de::DeError::Custom(
934            "No patient found in chunk".to_string(),
935        ))
936    })
937}
938
939fn extract_site_chunks(xml: &str) -> Vec<&str> {
940    let mut chunks = Vec::new();
941    let mut pos = 0;
942    loop {
943        match xml[pos..].find("<site ") {
944            None => break,
945            Some(rel) => {
946                let start = pos + rel;
947                match xml[start..].find("</site>") {
948                    None => break,
949                    Some(rel_end) => {
950                        let end = start + rel_end + "</site>".len();
951                        chunks.push(&xml[start..end]);
952                        pos = end;
953                    }
954                }
955            }
956        }
957    }
958    chunks
959}
960
961fn parse_site_xml(site_xml: &str) -> Result<Site, Error> {
962    let mut xml_reader = Reader::from_str(site_xml);
963    xml_reader.config_mut().trim_text(true);
964
965    let mut current_site: Option<Site> = None;
966    let mut current_forms: Vec<Form> = Vec::new();
967    let mut current_form: Option<Form> = None;
968    let mut current_states: Vec<State> = Vec::new();
969    let mut current_categories: Vec<Category> = Vec::new();
970    let mut current_category: Option<Category> = None;
971    let mut current_fields: Vec<Field> = Vec::new();
972    let mut current_field: Option<Field> = None;
973    let mut current_entries: Vec<Entry> = Vec::new();
974    let mut current_entry: Option<Entry> = None;
975    let mut current_comments: Vec<Comment> = Vec::new();
976    let mut current_comment: Option<Comment> = None;
977    let mut current_value: Option<Value> = None;
978    let mut current_reason: Option<Reason> = None;
979    let mut text_content = String::new();
980    let mut in_form = false;
981    let mut in_category = false;
982    let mut in_field = false;
983    let mut in_entry = false;
984    let mut in_comment = false;
985    let mut in_value = false;
986    let mut in_reason = false;
987
988    loop {
989        match xml_reader.read_event() {
990            Err(e) => {
991                return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
992                    format!("XML reading error: {}", e),
993                )))
994            }
995            Ok(Event::Eof) => break,
996
997            Ok(Event::Start(ref e)) => {
998                let name_bytes = e.local_name();
999                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
1000                    match name {
1001                        "site" => {
1002                            let attrs = extract_attributes(e)?;
1003                            current_site = Some(Site::from_attributes(attrs)?);
1004                            current_forms.clear();
1005                        }
1006                        "form" if current_site.is_some() => {
1007                            let attrs = extract_attributes(e)?;
1008                            current_form = Some(Form::from_attributes(attrs)?);
1009                            in_form = true;
1010                            current_states.clear();
1011                            current_categories.clear();
1012                        }
1013                        "category" if in_form => {
1014                            let attrs = extract_attributes(e)?;
1015                            current_category = Some(Category::from_attributes(attrs)?);
1016                            in_category = true;
1017                            current_fields.clear();
1018                        }
1019                        "field" if in_category => {
1020                            let attrs = extract_attributes(e)?;
1021                            current_field = Some(Field::from_attributes(attrs)?);
1022                            in_field = true;
1023                            current_entries.clear();
1024                            current_comments.clear();
1025                        }
1026                        "entry" if in_field => {
1027                            let attrs = extract_attributes(e)?;
1028                            current_entry = Some(Entry::from_attributes(attrs)?);
1029                            in_entry = true;
1030                        }
1031                        "comment" if in_field => {
1032                            let attrs = extract_attributes(e)?;
1033                            let comment_id = attrs.get("id").cloned().unwrap_or_default();
1034                            current_comment = Some(Comment {
1035                                comment_id: comment_id.to_string(),
1036                                value: None,
1037                            });
1038                            in_comment = true;
1039                        }
1040                        "value" if in_entry || in_comment => {
1041                            let attrs = extract_attributes(e)?;
1042                            current_value = Some(Value::from_attributes(attrs)?);
1043                            in_value = true;
1044                            text_content.clear();
1045                        }
1046                        "reason" if in_entry => {
1047                            let attrs = extract_attributes(e)?;
1048                            current_reason = Some(Reason::from_attributes(attrs)?);
1049                            in_reason = true;
1050                            text_content.clear();
1051                        }
1052                        _ => {}
1053                    }
1054                }
1055            }
1056
1057            Ok(Event::Text(e)) if (in_value || in_reason) => {
1058                text_content.push_str(&String::from_utf8_lossy(&e));
1059            }
1060
1061            Ok(Event::End(ref e)) => {
1062                let name_bytes = e.local_name();
1063                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
1064                    match name {
1065                        "site" => {
1066                            if let Some(mut site) = current_site.take() {
1067                                if !current_forms.is_empty() {
1068                                    site.set_forms(std::mem::take(&mut current_forms));
1069                                }
1070                                current_site = Some(site);
1071                            }
1072                        }
1073                        "form" if in_form => {
1074                            if let Some(mut form) = current_form.take() {
1075                                if !current_states.is_empty() {
1076                                    form.states = Some(std::mem::take(&mut current_states));
1077                                }
1078                                if !current_categories.is_empty() {
1079                                    form.categories = Some(std::mem::take(&mut current_categories));
1080                                }
1081                                current_forms.push(form);
1082                            }
1083                            in_form = false;
1084                        }
1085                        "category" if in_category => {
1086                            if let Some(mut category) = current_category.take() {
1087                                if !current_fields.is_empty() {
1088                                    category.fields = Some(std::mem::take(&mut current_fields));
1089                                }
1090                                current_categories.push(category);
1091                            }
1092                            in_category = false;
1093                        }
1094                        "field" if in_field => {
1095                            if let Some(mut field) = current_field.take() {
1096                                if !current_entries.is_empty() {
1097                                    field.entries = Some(std::mem::take(&mut current_entries));
1098                                }
1099                                if !current_comments.is_empty() {
1100                                    field.comments = Some(std::mem::take(&mut current_comments));
1101                                }
1102                                current_fields.push(field);
1103                            }
1104                            in_field = false;
1105                        }
1106                        "entry" if in_entry => {
1107                            if let Some(entry) = current_entry.take() {
1108                                current_entries.push(entry);
1109                            }
1110                            in_entry = false;
1111                        }
1112                        "comment" if in_comment => {
1113                            if let Some(comment) = current_comment.take() {
1114                                current_comments.push(comment);
1115                            }
1116                            in_comment = false;
1117                        }
1118                        "value" if in_value => {
1119                            if let Some(mut value) = current_value.take() {
1120                                value.value = std::mem::take(&mut text_content);
1121                                if let Some(ref mut entry) = current_entry {
1122                                    entry.value = Some(value);
1123                                } else if let Some(ref mut comment) = current_comment {
1124                                    comment.value = Some(value);
1125                                }
1126                            }
1127                            in_value = false;
1128                        }
1129                        "reason" if in_reason => {
1130                            if let Some(mut reason) = current_reason.take() {
1131                                reason.value = std::mem::take(&mut text_content);
1132                                if let Some(ref mut entry) = current_entry {
1133                                    entry.reason = Some(reason);
1134                                }
1135                            }
1136                            in_reason = false;
1137                        }
1138                        _ => {}
1139                    }
1140                }
1141            }
1142
1143            Ok(Event::Empty(ref e)) => {
1144                let name_bytes = e.local_name();
1145                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
1146                    match name {
1147                        "state" if in_form => {
1148                            let attrs = extract_attributes(e)?;
1149                            let state = State::from_attributes(attrs)?;
1150                            current_states.push(state);
1151                        }
1152                        "lockState" if in_form => {
1153                            let attrs = extract_attributes(e)?;
1154                            let lock_state = LockState::from_attributes(attrs)?;
1155                            if let Some(ref mut form) = current_form {
1156                                form.lock_state = Some(lock_state);
1157                            }
1158                        }
1159                        "field" if in_category => {
1160                            let attrs = extract_attributes(e)?;
1161                            let field = Field::from_attributes(attrs)?;
1162                            current_fields.push(field);
1163                        }
1164                        "value" if in_entry => {
1165                            let attrs = extract_attributes(e)?;
1166                            let value = Value::from_attributes(attrs)?;
1167                            if let Some(ref mut entry) = current_entry {
1168                                entry.value = Some(value);
1169                            }
1170                        }
1171                        "reason" if in_entry => {
1172                            let attrs = extract_attributes(e)?;
1173                            let reason = Reason::from_attributes(attrs)?;
1174                            if let Some(ref mut entry) = current_entry {
1175                                entry.reason = Some(reason);
1176                            }
1177                        }
1178                        _ => {}
1179                    }
1180                }
1181            }
1182
1183            _ => {}
1184        }
1185    }
1186
1187    current_site.ok_or_else(|| {
1188        Error::ParsingError(quick_xml::de::DeError::Custom(
1189            "No site found in chunk".to_string(),
1190        ))
1191    })
1192}
1193
1194/// Parses a Prelude native user XML file into a `UserNative` struct.
1195///
1196/// # Example
1197///
1198/// ```
1199/// use std::path::Path;
1200///
1201/// use prelude_xml_parser::parse_user_native_file;
1202///
1203/// let file_path = Path::new("tests/assets/user_native.xml");
1204/// let native = parse_user_native_file(&file_path).unwrap();
1205///
1206/// assert!(native.users.len() >= 1, "Vector length is less than 1");
1207/// ```
1208pub fn parse_user_native_file(xml_path: &Path) -> Result<UserNative, Error> {
1209    check_valid_xml_file(xml_path)?;
1210
1211    let xml_file = read_to_string(xml_path)?;
1212    let native = parse_user_native_string(&xml_file)?;
1213
1214    Ok(native)
1215}
1216
1217/// Parse a string of Prelude native user XML into a `UserNative` struct.
1218///
1219/// # Example
1220///
1221/// ```
1222/// use chrono::{DateTime, Utc};
1223/// use prelude_xml_parser::parse_user_native_string;
1224/// use prelude_xml_parser::native::user_native::*;
1225///
1226/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1227///   <export_from_vision_EDC date="02-Jun-2024 06:59 -0500" createdBy="Paul Sanders" role="Project Manager" numberSubjectsProcessed="3">
1228///     <user uniqueId="1691421275437" lastLanguage="" creator="Paul Sanders(1681162687395)" numberOfForms="1">
1229///       <form name="form.name.demographics" lastModified="2023-08-07 10:15:41 -0500" whoLastModifiedName="Paul Sanders" whoLastModifiedRole="Project Manager" whenCreated="1691421341578" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="User Demographics" formIndex="1" formGroup="" formState="In-Work">
1230///         <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-08-07 10:15:41 -0500" />
1231///         <category name="demographics" type="normal" highestIndex="0">
1232///           <field name="address" type="text" dataType="string" errorCode="undefined" whenCreated="2024-01-12 14:14:09 -0600" keepHistory="true" />
1233///           <field name="email" type="text" dataType="string" errorCode="undefined" whenCreated="2023-08-07 10:15:41 -0500" keepHistory="true">
1234///             <entry id="1">
1235///               <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-08-07 10:15:41 -0500" xml:space="preserve">jazz@artemis.com</value>
1236///             </entry>
1237///           </field>
1238///         </category>
1239///         <category name="Administrative" type="normal" highestIndex="0">
1240///           <field name="study_assignment" type="text" dataType="" errorCode="undefined" whenCreated="2023-08-07 10:15:41 -0500" keepHistory="true">
1241///             <entry id="1">
1242///               <value by="set from calculation" byUniqueId="" role="System" when="2023-08-07 10:15:41 -0500" xml:space="preserve">On 07-Aug-2023 10:15 -0500, Paul Sanders assigned user from another study</value>
1243///               <reason by="set from calculation" byUniqueId="" role="System" when="2023-08-07 10:15:41 -0500" xml:space="preserve">calculated value</reason>
1244///             </entry>
1245///           </field>
1246///         </category>
1247///       </form>
1248///     </user>
1249///   </export_from_vision_EDC>
1250/// "#;
1251///
1252/// let expected = UserNative {
1253///     users: vec![User {
1254///         unique_id: "1691421275437".to_string(),
1255///         last_language: None,
1256///         creator: "Paul Sanders(1681162687395)".to_string(),
1257///         number_of_forms: 1,
1258///         forms: Some(vec![Form {
1259///             name: "form.name.demographics".to_string(),
1260///             last_modified: Some(
1261///                 DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1262///                     .unwrap()
1263///                     .with_timezone(&Utc),
1264///             ),
1265///             who_last_modified_name: Some("Paul Sanders".to_string()),
1266///             who_last_modified_role: Some("Project Manager".to_string()),
1267///             when_created: 1691421341578,
1268///             has_errors: false,
1269///             has_warnings: false,
1270///             locked: false,
1271///             user: None,
1272///             date_time_changed: None,
1273///             form_title: "User Demographics".to_string(),
1274///             form_index: 1,
1275///             form_group: None,
1276///             form_state: "In-Work".to_string(),
1277///             lock_state: None,
1278///             states: Some(vec![State {
1279///                 value: "form.state.in.work".to_string(),
1280///                 signer: "Paul Sanders - Project Manager".to_string(),
1281///                 signer_unique_id: "1681162687395".to_string(),
1282///                 date_signed: Some(
1283///                     DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1284///                         .unwrap()
1285///                         .with_timezone(&Utc),
1286///                 ),
1287///             }]),
1288///             categories: Some(vec![
1289///                         Category {
1290///                             name: "demographics".to_string(),
1291///                             category_type: "normal".to_string(),
1292///                             highest_index: 0,
1293///                             fields: Some(vec![
1294///                                 Field {
1295///                                     name: "address".to_string(),
1296///                                     field_type: "text".to_string(),
1297///                                     data_type: Some("string".to_string()),
1298///                                     error_code: "undefined".to_string(),
1299///                                     when_created: Some(DateTime::parse_from_rfc3339("2024-01-12T20:14:09Z")
1300///                                         .unwrap()
1301///                                         .with_timezone(&Utc)),
1302///                                     keep_history: true,
1303///                                     entries: None,
1304///                                     comments: None,
1305///                                 },
1306///                                 Field {
1307///                                     name: "email".to_string(),
1308///                                     field_type: "text".to_string(),
1309///                                     data_type: Some("string".to_string()),
1310///                                     error_code: "undefined".to_string(),
1311///                                     when_created: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1312///                                         .unwrap()
1313///                                         .with_timezone(&Utc)),
1314///                                     keep_history: true,
1315///                                     entries: Some(vec![Entry {
1316///                                         entry_id: "1".to_string(),
1317///                                         reviewed_by: None,
1318///                                         reviewed_by_unique_id: None,
1319///                                         reviewed_by_when: None,
1320///                                         value: Some(Value {
1321///                                             by: "Paul Sanders".to_string(),
1322///                                             by_unique_id: Some("1681162687395".to_string()),
1323///                                             role: "Project Manager".to_string(),
1324///                                             when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1325///                                                 .unwrap()
1326///                                                 .with_timezone(&Utc)),
1327///                                             value: "jazz@artemis.com".to_string(),
1328///                                         }),
1329///                                         reason: None,
1330///                                     }]),
1331///                                     comments: None,
1332///                                 },
1333///                             ]),
1334///                         },
1335///                         Category {
1336///                             name: "Administrative".to_string(),
1337///                             category_type: "normal".to_string(),
1338///                             highest_index: 0,
1339///                             fields: Some(vec![
1340///                                 Field {
1341///                                     name: "study_assignment".to_string(),
1342///                                     field_type: "text".to_string(),
1343///                                     data_type: None,
1344///                                     error_code: "undefined".to_string(),
1345///                                     when_created: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1346///                                         .unwrap()
1347///                                         .with_timezone(&Utc)),
1348///                                     keep_history: true,
1349///                                     entries: Some(vec![
1350///                                         Entry {
1351///                                             entry_id: "1".to_string(),
1352///                                             reviewed_by: None,
1353///                                             reviewed_by_unique_id: None,
1354///                                             reviewed_by_when: None,
1355///                                             value: Some(Value {
1356///                                                 by: "set from calculation".to_string(),
1357///                                                 by_unique_id: None,
1358///                                                 role: "System".to_string(),
1359///                                                 when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1360///                                                     .unwrap()
1361///                                                     .with_timezone(&Utc)),
1362///                                                 value: "On 07-Aug-2023 10:15 -0500, Paul Sanders assigned user from another study".to_string(),
1363///                                             }),
1364///                                             reason: Some(Reason {
1365///                                                 by: "set from calculation".to_string(),
1366///                                                 by_unique_id: None,
1367///                                                 role: "System".to_string(),
1368///                                                 when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1369///                                                     .unwrap()
1370///                                                     .with_timezone(&Utc)),
1371///                                                 value: "calculated value".to_string(),
1372///                                             }),
1373///                                         },
1374///                                     ]),
1375///                                     comments: None,
1376///                                 },
1377///                             ]),
1378///                         },
1379///             ]),
1380///         }]),
1381///     }],
1382/// };
1383///
1384/// let result = parse_user_native_string(xml).unwrap();
1385///
1386/// assert_eq!(result, expected);
1387/// ```
1388pub fn parse_user_native_string(xml_str: &str) -> Result<UserNative, Error> {
1389    let chunks = extract_user_chunks(xml_str);
1390    let users = chunks
1391        .into_par_iter()
1392        .map(parse_user_xml)
1393        .collect::<Result<Vec<_>, _>>()?;
1394    Ok(UserNative { users })
1395}
1396
1397fn extract_user_chunks(xml: &str) -> Vec<&str> {
1398    let mut chunks = Vec::new();
1399    let mut pos = 0;
1400    loop {
1401        match xml[pos..].find("<user ") {
1402            None => break,
1403            Some(rel) => {
1404                let start = pos + rel;
1405                match xml[start..].find("</user>") {
1406                    None => break,
1407                    Some(rel_end) => {
1408                        let end = start + rel_end + "</user>".len();
1409                        chunks.push(&xml[start..end]);
1410                        pos = end;
1411                    }
1412                }
1413            }
1414        }
1415    }
1416    chunks
1417}
1418
1419fn parse_user_xml(user_xml: &str) -> Result<User, Error> {
1420    let mut xml_reader = Reader::from_str(user_xml);
1421    xml_reader.config_mut().trim_text(true);
1422
1423    let mut current_user: Option<User> = None;
1424    let mut current_forms: Vec<Form> = Vec::new();
1425    let mut current_form: Option<Form> = None;
1426    let mut current_states: Vec<State> = Vec::new();
1427    let mut current_categories: Vec<Category> = Vec::new();
1428    let mut current_category: Option<Category> = None;
1429    let mut current_fields: Vec<Field> = Vec::new();
1430    let mut current_field: Option<Field> = None;
1431    let mut current_entries: Vec<Entry> = Vec::new();
1432    let mut current_entry: Option<Entry> = None;
1433    let mut current_comments: Vec<Comment> = Vec::new();
1434    let mut current_comment: Option<Comment> = None;
1435    let mut current_value: Option<Value> = None;
1436    let mut current_reason: Option<Reason> = None;
1437    let mut text_content = String::new();
1438    let mut in_form = false;
1439    let mut in_category = false;
1440    let mut in_field = false;
1441    let mut in_entry = false;
1442    let mut in_comment = false;
1443    let mut in_value = false;
1444    let mut in_reason = false;
1445
1446    loop {
1447        match xml_reader.read_event() {
1448            Err(e) => {
1449                return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
1450                    format!("XML reading error: {}", e),
1451                )))
1452            }
1453            Ok(Event::Eof) => break,
1454
1455            Ok(Event::Start(ref e)) => {
1456                let name_bytes = e.local_name();
1457                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
1458                    match name {
1459                        "user" => {
1460                            let attrs = extract_attributes(e)?;
1461                            current_user = Some(User::from_attributes(attrs)?);
1462                        }
1463                        "form" => {
1464                            let attrs = extract_attributes(e)?;
1465                            current_form = Some(Form::from_attributes(attrs)?);
1466                            in_form = true;
1467                        }
1468                        "category" if in_form => {
1469                            let attrs = extract_attributes(e)?;
1470                            current_category = Some(Category::from_attributes(attrs)?);
1471                            in_category = true;
1472                        }
1473                        "field" if in_category => {
1474                            let attrs = extract_attributes(e)?;
1475                            current_field = Some(Field::from_attributes(attrs)?);
1476                            in_field = true;
1477                        }
1478                        "entry" if in_field => {
1479                            let attrs = extract_attributes(e)?;
1480                            current_entry = Some(Entry::from_attributes(attrs)?);
1481                            in_entry = true;
1482                        }
1483                        "comment" if in_field => {
1484                            let attrs = extract_attributes(e)?;
1485                            let comment_id = attrs.get("id").cloned().unwrap_or_default();
1486                            current_comment = Some(Comment {
1487                                comment_id: comment_id.to_string(),
1488                                value: None,
1489                            });
1490                            in_comment = true;
1491                        }
1492                        "value" if in_entry || in_comment => {
1493                            let attrs = extract_attributes(e)?;
1494                            current_value = Some(Value::from_attributes(attrs)?);
1495                            in_value = true;
1496                            text_content.clear();
1497                        }
1498                        "reason" if in_entry => {
1499                            let attrs = extract_attributes(e)?;
1500                            current_reason = Some(Reason::from_attributes(attrs)?);
1501                            in_reason = true;
1502                            text_content.clear();
1503                        }
1504                        _ => {}
1505                    }
1506                }
1507            }
1508
1509            Ok(Event::Text(e)) if (in_value || in_reason) => {
1510                text_content.push_str(&String::from_utf8_lossy(&e));
1511            }
1512
1513            Ok(Event::End(ref e)) => {
1514                let name_bytes = e.local_name();
1515                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
1516                    match name {
1517                        "user" => {
1518                            if let Some(mut user) = current_user.take() {
1519                                if !current_forms.is_empty() {
1520                                    user.set_forms(std::mem::take(&mut current_forms));
1521                                }
1522                                current_user = Some(user);
1523                            }
1524                        }
1525                        "form" if in_form => {
1526                            if let Some(mut form) = current_form.take() {
1527                                if !current_states.is_empty() {
1528                                    form.states = Some(std::mem::take(&mut current_states));
1529                                }
1530                                if !current_categories.is_empty() {
1531                                    form.categories = Some(std::mem::take(&mut current_categories));
1532                                }
1533                                current_forms.push(form);
1534                            }
1535                            in_form = false;
1536                        }
1537                        "category" if in_category => {
1538                            if let Some(mut category) = current_category.take() {
1539                                if !current_fields.is_empty() {
1540                                    category.fields = Some(std::mem::take(&mut current_fields));
1541                                }
1542                                current_categories.push(category);
1543                            }
1544                            in_category = false;
1545                        }
1546                        "field" if in_field => {
1547                            if let Some(mut field) = current_field.take() {
1548                                if !current_entries.is_empty() {
1549                                    field.entries = Some(std::mem::take(&mut current_entries));
1550                                }
1551                                if !current_comments.is_empty() {
1552                                    field.comments = Some(std::mem::take(&mut current_comments));
1553                                }
1554                                current_fields.push(field);
1555                            }
1556                            in_field = false;
1557                        }
1558                        "entry" if in_entry => {
1559                            if let Some(entry) = current_entry.take() {
1560                                current_entries.push(entry);
1561                            }
1562                            in_entry = false;
1563                        }
1564                        "comment" if in_comment => {
1565                            if let Some(comment) = current_comment.take() {
1566                                current_comments.push(comment);
1567                            }
1568                            in_comment = false;
1569                        }
1570                        "value" if in_value => {
1571                            if let Some(mut value) = current_value.take() {
1572                                value.value = std::mem::take(&mut text_content);
1573                                if let Some(ref mut entry) = current_entry {
1574                                    entry.value = Some(value);
1575                                } else if let Some(ref mut comment) = current_comment {
1576                                    comment.value = Some(value);
1577                                }
1578                            }
1579                            in_value = false;
1580                        }
1581                        "reason" if in_reason => {
1582                            if let Some(mut reason) = current_reason.take() {
1583                                reason.value = std::mem::take(&mut text_content);
1584                                if let Some(ref mut entry) = current_entry {
1585                                    entry.reason = Some(reason);
1586                                }
1587                            }
1588                            in_reason = false;
1589                        }
1590                        _ => {}
1591                    }
1592                }
1593            }
1594
1595            Ok(Event::Empty(ref e)) => {
1596                let name_bytes = e.local_name();
1597                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
1598                    match name {
1599                        "state" if in_form => {
1600                            let attrs = extract_attributes(e)?;
1601                            let state = State::from_attributes(attrs)?;
1602                            current_states.push(state);
1603                        }
1604                        "field" if in_category => {
1605                            let attrs = extract_attributes(e)?;
1606                            let field = Field::from_attributes(attrs)?;
1607                            current_fields.push(field);
1608                        }
1609                        _ => {}
1610                    }
1611                }
1612            }
1613
1614            _ => {}
1615        }
1616    }
1617
1618    current_user.ok_or_else(|| {
1619        Error::ParsingError(quick_xml::de::DeError::Custom(
1620            "No user element found".to_string(),
1621        ))
1622    })
1623}
1624
1625fn check_valid_xml_file(xml_path: &Path) -> Result<(), Error> {
1626    if !xml_path.exists() {
1627        return Err(Error::FileNotFound(xml_path.to_path_buf()));
1628    }
1629
1630    if let Some(extension) = xml_path.extension() {
1631        if extension != "xml" {
1632            return Err(Error::InvalidFileType(xml_path.to_owned()));
1633        }
1634    } else {
1635        return Err(Error::Unknown);
1636    }
1637
1638    Ok(())
1639}
1640
1641#[cfg(test)]
1642mod tests {
1643    use super::*;
1644    use tempfile::{tempdir, Builder};
1645
1646    #[test]
1647    fn test_site_file_not_found_error() {
1648        let dir = tempdir().unwrap().path().to_path_buf();
1649        let result = parse_site_native_file(&dir);
1650        assert!(result.is_err());
1651        assert!(matches!(result, Err(Error::FileNotFound(_))));
1652    }
1653
1654    #[test]
1655    fn test_site_invalid_file_type_error() {
1656        let file = Builder::new()
1657            .prefix("test")
1658            .suffix(".csv")
1659            .tempfile()
1660            .unwrap();
1661        let result = parse_site_native_file(file.path());
1662
1663        assert!(result.is_err());
1664        assert!(matches!(result, Err(Error::InvalidFileType(_))));
1665    }
1666
1667    #[test]
1668    fn test_subject_file_not_found_error() {
1669        let dir = tempdir().unwrap().path().to_path_buf();
1670        let result = parse_subject_native_file(&dir);
1671        assert!(result.is_err());
1672        assert!(matches!(result, Err(Error::FileNotFound(_))));
1673    }
1674
1675    #[test]
1676    fn test_subject_invalid_file_type_error() {
1677        let file = Builder::new()
1678            .prefix("test")
1679            .suffix(".csv")
1680            .tempfile()
1681            .unwrap();
1682        let result = parse_subject_native_file(file.path());
1683
1684        assert!(result.is_err());
1685        assert!(matches!(result, Err(Error::InvalidFileType(_))));
1686    }
1687
1688    #[test]
1689    fn test_user_file_not_found_error() {
1690        let dir = tempdir().unwrap().path().to_path_buf();
1691        let result = parse_user_native_file(&dir);
1692        assert!(result.is_err());
1693        assert!(matches!(result, Err(Error::FileNotFound(_))));
1694    }
1695
1696    #[test]
1697    fn test_user_invalid_file_type_error() {
1698        let file = Builder::new()
1699            .prefix("test")
1700            .suffix(".csv")
1701            .tempfile()
1702            .unwrap();
1703        let result = parse_user_native_file(file.path());
1704
1705        assert!(result.is_err());
1706        assert!(matches!(result, Err(Error::InvalidFileType(_))));
1707    }
1708
1709    #[test]
1710    fn test_forms_parsing_regression() {
1711        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1712<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1713    <patient patientId="TEST-001" uniqueId="123456789" whenCreated="2023-04-15 12:09:02 -0400" creator="Test User" siteName="Test Site" siteUniqueId="987654321" lastLanguage="English" numberOfForms="2">
1714        <form name="test.form.1" lastModified="2023-04-15 12:09:15 -0400" whoLastModifiedName="Test User" whoLastModifiedRole="Tester" whenCreated="123456789" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Test Form 1" formIndex="1" formGroup="Test Group" formState="In-Work">
1715            <state value="form.state.in.work" signer="Test User - Tester" signerUniqueId="111111111" dateSigned="2023-04-15 12:09:02 -0400"/>
1716            <category name="Test Category" type="normal" highestIndex="0">
1717                <field name="test_field" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
1718                    <entry id="1">
1719                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Test Value</value>
1720                    </entry>
1721                </field>
1722            </category>
1723        </form>
1724        <form name="test.form.2" lastModified="2023-04-15 12:10:15 -0400" whoLastModifiedName="Test User" whoLastModifiedRole="Tester" whenCreated="123456790" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Test Form 2" formIndex="2" formGroup="Test Group" formState="Complete">
1725            <state value="form.state.complete" signer="Test User - Tester" signerUniqueId="111111111" dateSigned="2023-04-15 12:10:02 -0400"/>
1726        </form>
1727    </patient>
1728</export_from_vision_EDC>"#;
1729
1730        let result = parse_subject_native_string(xml).expect("Should parse successfully");
1731
1732        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1733
1734        let patient = &result.patients[0];
1735        assert_eq!(patient.patient_id, "TEST-001");
1736        assert_eq!(patient.number_of_forms, 2);
1737
1738        let forms = patient.forms.as_ref().expect("Patient should have forms");
1739        assert_eq!(forms.len(), 2, "Patient should have exactly 2 forms");
1740
1741        let form1 = &forms[0];
1742        assert_eq!(form1.name, "test.form.1");
1743        assert_eq!(form1.form_title, "Test Form 1");
1744        assert_eq!(form1.form_index, 1);
1745        assert_eq!(form1.form_state, "In-Work");
1746
1747        let states1 = form1.states.as_ref().expect("Form 1 should have states");
1748        assert_eq!(states1.len(), 1);
1749        assert_eq!(states1[0].value, "form.state.in.work");
1750
1751        let categories1 = form1
1752            .categories
1753            .as_ref()
1754            .expect("Form 1 should have categories");
1755        assert_eq!(categories1.len(), 1);
1756        assert_eq!(categories1[0].name, "Test Category");
1757
1758        let fields1 = categories1[0]
1759            .fields
1760            .as_ref()
1761            .expect("Category should have fields");
1762        assert_eq!(fields1.len(), 1);
1763        assert_eq!(fields1[0].name, "test_field");
1764
1765        let entries1 = fields1[0]
1766            .entries
1767            .as_ref()
1768            .expect("Field should have entries");
1769        assert_eq!(entries1.len(), 1);
1770        assert_eq!(entries1[0].entry_id, "1");
1771
1772        let value1 = entries1[0].value.as_ref().expect("Entry should have value");
1773        assert_eq!(value1.value, "Test Value");
1774        assert_eq!(value1.by, "Test User");
1775        assert_eq!(value1.role, "Tester");
1776
1777        let form2 = &forms[1];
1778        assert_eq!(form2.name, "test.form.2");
1779        assert_eq!(form2.form_title, "Test Form 2");
1780        assert_eq!(form2.form_index, 2);
1781        assert_eq!(form2.form_state, "Complete");
1782
1783        let states2 = form2.states.as_ref().expect("Form 2 should have states");
1784        assert_eq!(states2.len(), 1);
1785        assert_eq!(states2[0].value, "form.state.complete");
1786    }
1787
1788    #[test]
1789    fn test_comments_parsing_regression() {
1790        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1791<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1792    <patient patientId="TEST-002" uniqueId="123456790" whenCreated="2023-04-15 12:09:02 -0400" creator="Test User" siteName="Test Site" siteUniqueId="987654321" lastLanguage="English" numberOfForms="1">
1793        <form name="test.form.with.comments" lastModified="2023-04-15 12:09:15 -0400" whoLastModifiedName="Test User" whoLastModifiedRole="Tester" whenCreated="123456789" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Test Form With Comments" formIndex="1" formGroup="Test Group" formState="In-Work">
1794            <category name="Test Category" type="normal" highestIndex="0">
1795                <field name="field_with_comments" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
1796                    <entry id="1">
1797                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Field Value</value>
1798                    </entry>
1799                    <comment id="1">
1800                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:05 -0400" xml:space="preserve">First comment</value>
1801                    </comment>
1802                    <comment id="2">
1803                        <value by="Another User" byUniqueId="222222222" role="Reviewer" when="2023-04-15 12:10:00 -0400" xml:space="preserve">Second comment</value>
1804                    </comment>
1805                </field>
1806                <field name="field_without_comments" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:30 -0400" keepHistory="true">
1807                    <entry id="1">
1808                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:10 -0400" xml:space="preserve">Another Value</value>
1809                    </entry>
1810                </field>
1811            </category>
1812        </form>
1813    </patient>
1814</export_from_vision_EDC>"#;
1815
1816        let result = parse_subject_native_string(xml).expect("Should parse successfully");
1817
1818        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1819
1820        let patient = &result.patients[0];
1821        let forms = patient.forms.as_ref().expect("Patient should have forms");
1822        let form = &forms[0];
1823        let categories = form
1824            .categories
1825            .as_ref()
1826            .expect("Form should have categories");
1827        let fields = categories[0]
1828            .fields
1829            .as_ref()
1830            .expect("Category should have fields");
1831        assert_eq!(fields.len(), 2, "Should have 2 fields");
1832
1833        let field_with_comments = &fields[0];
1834        assert_eq!(field_with_comments.name, "field_with_comments");
1835
1836        let comments = field_with_comments
1837            .comments
1838            .as_ref()
1839            .expect("Field should have comments");
1840        assert_eq!(comments.len(), 2, "Should have exactly 2 comments");
1841
1842        let comment1 = &comments[0];
1843        assert_eq!(comment1.comment_id, "1");
1844        let comment1_value = comment1
1845            .value
1846            .as_ref()
1847            .expect("Comment 1 should have value");
1848        assert_eq!(comment1_value.value, "First comment");
1849        assert_eq!(comment1_value.by, "Test User");
1850        assert_eq!(comment1_value.role, "Tester");
1851
1852        let comment2 = &comments[1];
1853        assert_eq!(comment2.comment_id, "2");
1854        let comment2_value = comment2
1855            .value
1856            .as_ref()
1857            .expect("Comment 2 should have value");
1858        assert_eq!(comment2_value.value, "Second comment");
1859        assert_eq!(comment2_value.by, "Another User");
1860        assert_eq!(comment2_value.role, "Reviewer");
1861
1862        let field_without_comments = &fields[1];
1863        assert_eq!(field_without_comments.name, "field_without_comments");
1864        assert!(
1865            field_without_comments.comments.is_none(),
1866            "Field without comments should have no comments"
1867        );
1868    }
1869
1870    #[test]
1871    fn test_empty_forms_handling() {
1872        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1873<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1874    <patient patientId="TEST-003" uniqueId="123456791" whenCreated="2023-04-15 12:09:02 -0400" creator="Test User" siteName="Test Site" siteUniqueId="987654321" lastLanguage="English" numberOfForms="0">
1875    </patient>
1876</export_from_vision_EDC>"#;
1877
1878        let result = parse_subject_native_string(xml).expect("Should parse successfully");
1879
1880        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1881
1882        let patient = &result.patients[0];
1883        assert_eq!(patient.patient_id, "TEST-003");
1884        assert_eq!(patient.number_of_forms, 0);
1885        assert!(
1886            patient.forms.is_none(),
1887            "Patient with 0 forms should have None for forms"
1888        );
1889    }
1890
1891    #[test]
1892    fn test_large_patient_forms_regression() {
1893        let mut xml = String::from(
1894            r#"<?xml version="1.0" encoding="UTF-8"?>
1895<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1896    <patient patientId="LARGE-TEST" uniqueId="123456792" whenCreated="2023-04-15 12:09:02 -0400" creator="Test User" siteName="Test Site" siteUniqueId="987654321" lastLanguage="English" numberOfForms="50">"#,
1897        );
1898
1899        for i in 1..=50 {
1900            xml.push_str(&format!(r#"
1901        <form name="test.form.{}" lastModified="2023-04-15 12:09:15 -0400" whoLastModifiedName="Test User" whoLastModifiedRole="Tester" whenCreated="12345678{}" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Test Form {}" formIndex="{}" formGroup="Test Group" formState="In-Work">
1902            <state value="form.state.in.work" signer="Test User - Tester" signerUniqueId="111111111" dateSigned="2023-04-15 12:09:02 -0400"/>
1903            <category name="Category {}" type="normal" highestIndex="0">
1904                <field name="field_{}" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
1905                    <entry id="1">
1906                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Value {}</value>
1907                    </entry>
1908                    <comment id="1">
1909                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:05 -0400" xml:space="preserve">Comment for form {}</value>
1910                    </comment>
1911                </field>
1912            </category>
1913        </form>"#, i, i, i, i, i, i, i, i));
1914        }
1915
1916        xml.push_str(
1917            r#"
1918    </patient>
1919</export_from_vision_EDC>"#,
1920        );
1921
1922        let result =
1923            parse_subject_native_string(&xml).expect("Should parse large patient successfully");
1924
1925        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1926
1927        let patient = &result.patients[0];
1928        assert_eq!(patient.patient_id, "LARGE-TEST");
1929        assert_eq!(patient.number_of_forms, 50);
1930
1931        let forms = patient.forms.as_ref().expect("Patient should have forms");
1932        assert_eq!(forms.len(), 50, "Patient should have exactly 50 forms");
1933
1934        for (i, form) in forms.iter().enumerate() {
1935            let form_num = i + 1;
1936            assert_eq!(form.name, format!("test.form.{}", form_num));
1937            assert_eq!(form.form_title, format!("Test Form {}", form_num));
1938            assert_eq!(form.form_index, form_num);
1939
1940            let categories = form
1941                .categories
1942                .as_ref()
1943                .expect("Form should have categories");
1944            assert_eq!(categories.len(), 1);
1945
1946            let fields = categories[0]
1947                .fields
1948                .as_ref()
1949                .expect("Category should have fields");
1950            assert_eq!(fields.len(), 1);
1951
1952            let entries = fields[0]
1953                .entries
1954                .as_ref()
1955                .expect("Field should have entries");
1956            assert_eq!(entries.len(), 1);
1957            assert_eq!(
1958                entries[0].value.as_ref().unwrap().value,
1959                format!("Value {}", form_num)
1960            );
1961
1962            let comments = fields[0]
1963                .comments
1964                .as_ref()
1965                .expect("Field should have comments");
1966            assert_eq!(comments.len(), 1);
1967            assert_eq!(
1968                comments[0].value.as_ref().unwrap().value,
1969                format!("Comment for form {}", form_num)
1970            );
1971        }
1972    }
1973
1974    #[test]
1975    fn test_malformed_datetime_handling() {
1976        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1977<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1978    <patient patientId="TEST-004" uniqueId="123456793" whenCreated="" creator="Test User" siteName="Test Site" siteUniqueId="987654321" lastLanguage="English" numberOfForms="1">
1979        <form name="test.form.malformed.dates" lastModified="" whoLastModifiedName="Test User" whoLastModifiedRole="Tester" whenCreated="123456789" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Test Form" formIndex="1" formGroup="Test Group" formState="In-Work">
1980            <category name="Test Category" type="normal" highestIndex="0">
1981                <field name="test_field" type="text" dataType="string" errorCode="valid" whenCreated="" keepHistory="true">
1982                    <entry id="1">
1983                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Test Value</value>
1984                    </entry>
1985                </field>
1986            </category>
1987        </form>
1988    </patient>
1989</export_from_vision_EDC>"#;
1990
1991        let result =
1992            parse_subject_native_string(xml).expect("Should handle malformed datetimes gracefully");
1993
1994        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1995
1996        let patient = &result.patients[0];
1997        assert!(
1998            patient.when_created.is_none(),
1999            "Empty whenCreated should be None"
2000        );
2001
2002        let forms = patient.forms.as_ref().expect("Patient should have forms");
2003        let form = &forms[0];
2004        assert!(
2005            form.last_modified.is_none(),
2006            "Empty lastModified should be None"
2007        );
2008
2009        let categories = form
2010            .categories
2011            .as_ref()
2012            .expect("Form should have categories");
2013        let fields = categories[0]
2014            .fields
2015            .as_ref()
2016            .expect("Category should have fields");
2017        let field = &fields[0];
2018        assert!(
2019            field.when_created.is_none(),
2020            "Empty whenCreated in field should be None"
2021        );
2022    }
2023
2024    #[test]
2025    fn test_empty_datetime_in_value_and_reason() {
2026        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
2027<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
2028  <patient patientId="TEST-001" uniqueId="123456" whenCreated="2023-04-15 12:09:02 -0400" creator="Test User" siteName="Test Site" siteUniqueId="654321" lastLanguage="" numberOfForms="1">
2029    <form name="test.form" lastModified="2023-04-15 12:09:15 -0400" whoLastModifiedName="Test User" whoLastModifiedRole="Manager" whenCreated="1681574905839" hasErrors="false" hasWarnings="false" locked="false" user="" dateTimeChanged="" formTitle="Test Form" formIndex="1" formGroup="Test" formState="In-Work">
2030      <state value="form.state.in.work" signer="Test User - Manager" signerUniqueId="123456" dateSigned="2023-04-15 12:09:02 -0400" />
2031      <category name="Test Category" type="normal" highestIndex="0">
2032        <field name="test_field" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
2033          <entry id="1">
2034            <value by="Test User" byUniqueId="123456" role="Manager" when="" xml:space="preserve">Test Value</value>
2035            <reason by="Test User" byUniqueId="123456" role="Manager" when="" xml:space="preserve">Test Reason</reason>
2036          </entry>
2037        </field>
2038      </category>
2039    </form>
2040  </patient>
2041</export_from_vision_EDC>"#;
2042
2043        let result = parse_subject_native_string(xml);
2044        assert!(result.is_ok(), "Should parse successfully: {:?}", result);
2045
2046        let native = result.unwrap();
2047        assert_eq!(native.patients.len(), 1, "Should have 1 patient");
2048
2049        let patient = &native.patients[0];
2050        let forms = patient.forms.as_ref().expect("Patient should have forms");
2051        let form = &forms[0];
2052        let categories = form
2053            .categories
2054            .as_ref()
2055            .expect("Form should have categories");
2056        let fields = categories[0]
2057            .fields
2058            .as_ref()
2059            .expect("Category should have fields");
2060        let field = &fields[0];
2061        let entries = field.entries.as_ref().expect("Field should have entries");
2062        let entry = &entries[0];
2063
2064        let value = entry.value.as_ref().expect("Entry should have value");
2065        assert!(
2066            value.when.is_none(),
2067            "Empty when attribute in value should be None"
2068        );
2069        assert_eq!(value.value, "Test Value");
2070
2071        let reason = entry.reason.as_ref().expect("Entry should have reason");
2072        assert!(
2073            reason.when.is_none(),
2074            "Empty when attribute in reason should be None"
2075        );
2076        assert_eq!(reason.value, "Test Reason");
2077    }
2078}