Skip to main content

prelude_xml_parser/
lib.rs

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