prelude_xml_parser/
lib.rs

1pub mod errors;
2pub mod native;
3
4use std::{
5    collections::HashMap,
6    fs::{read_to_string, File},
7    io::{BufReader, Cursor},
8    path::Path,
9};
10
11use crate::errors::Error;
12use crate::native::{
13    common::{Category, Comment, Entry, Field, LockState, Reason, State, Value},
14    site_native::SiteNative,
15    subject_native::{Form, Patient, SubjectNative},
16    user_native::UserNative,
17};
18use quick_xml::events::{BytesStart, Event};
19use quick_xml::Reader;
20
21/// Parses a Prelude native XML file into a `Native` stuct.
22///
23/// # Example
24///
25/// ```
26/// use std::path::Path;
27///
28/// use prelude_xml_parser::parse_site_native_file;
29///
30/// let file_path = Path::new("tests/assets/site_native.xml");
31/// let native = parse_site_native_file(&file_path).unwrap();
32///
33/// assert!(native.sites.len() >= 1, "Vector length is less than 1");
34/// ```
35pub fn parse_site_native_file(xml_path: &Path) -> Result<SiteNative, Error> {
36    check_valid_xml_file(xml_path)?;
37
38    let xml_file = read_to_string(xml_path)?;
39    let native = parse_site_native_string(&xml_file)?;
40
41    Ok(native)
42}
43
44/// Parse a string of Prelude native site XML into a `SiteNative` struct.
45///
46/// # Example
47///
48/// ```
49/// use chrono::{DateTime, Utc};
50/// use prelude_xml_parser::parse_site_native_string;
51/// use prelude_xml_parser::native::site_native::*;
52///
53/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
54/// <export_from_vision_EDC date="01-Jun-2024 18:17 -0500" createdBy="Paul Sanders" role="Project Manager" numberSubjectsProcessed="2">
55///
56///   <site name="Some Site" uniqueId="1681574834910" numberOfPatients="4" countOfRandomizedPatients="0" whenCreated="2023-04-15 12:08:19 -0400" creator="Paul Sanders" numberOfForms="1">
57///     <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">
58///       <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-04-15 12:08:19 -0400" />
59///       <category name="Demographics" type="normal" highestIndex="0">
60///         <field name="address" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true" />
61///         <field name="company" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true">
62///           <entry id="1">
63///             <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-04-15 12:08:19 -0400" xml:space="preserve">Some Company</value>
64///           </entry>
65///         </field>
66///         <field name="site_code_name" type="hidden" dataType="string" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true">
67///           <entry id="1">
68///             <value by="set from calculation" byUniqueId="" role="System" when="2023-04-15 12:08:19 -0400" xml:space="preserve">ABC-Some Site</value>
69///             <reason by="set from calculation" byUniqueId="" role="System" when="2023-04-15 12:08:19 -0400" xml:space="preserve">calculated value</reason>
70///           </entry>
71///           <entry id="2">
72///             <value by="set from calculation" byUniqueId="" role="System" when="2023-04-15 12:07:24 -0400" xml:space="preserve">Some Site</value>
73///             <reason by="set from calculation" byUniqueId="" role="System" when="2023-04-15 12:07:24 -0400" xml:space="preserve">calculated value</reason>
74///           </entry>
75///         </field>
76///       </category>
77///       <category name="Enrollment" type="normal" highestIndex="0">
78///         <field name="enrollment_closed_date" type="popUpCalendar" dataType="date" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true" />
79///         <field name="enrollment_open" type="radio" dataType="string" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true">
80///           <entry id="1">
81///             <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-04-15 12:08:19 -0400" xml:space="preserve">Yes</value>
82///           </entry>
83///         </field>
84///         <field name="enrollment_open_date" type="popUpCalendar" dataType="date" errorCode="valid" whenCreated="2023-04-15 11:07:14 -0500" keepHistory="true" />
85///       </category>
86///     </form>
87///   </site>
88///
89///   <site name="Artemis" uniqueId="1691420994591" numberOfPatients="0" countOfRandomizedPatients="0" whenCreated="2023-08-07 08:14:23 -0700" creator="Paul Sanders" numberOfForms="1">
90///     <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">
91///       <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-08-07 08:14:23 -0700" />
92///       <category name="Demographics" type="normal" highestIndex="0">
93///         <field name="address" type="text" dataType="string" errorCode="valid" whenCreated="2023-08-07 10:09:54 -0500" keepHistory="true">
94///           <entry id="1">
95///             <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-08-07 08:14:21 -0700" xml:space="preserve">1111 Moon Drive</value>
96///           </entry>
97///           <comment id="1">
98///             <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-08-07 08:14:21 -0700" xml:space="preserve">Some comment</value>
99///           </comment>
100///         </field>
101///       </category>
102///     </form>
103///   </site>
104///
105/// </export_from_vision_EDC>
106/// "#;
107///
108/// let expected = SiteNative {
109///     sites: vec![
110///         Site {
111///             name: "Some Site".to_string(),
112///             unique_id: "1681574834910".to_string(),
113///             number_of_patients: 4,
114///             count_of_randomized_patients: 0,
115///             when_created: Some(DateTime::parse_from_rfc3339("2023-04-15T16:08:19Z")
116///                 .unwrap()
117///                 .with_timezone(&Utc)),
118///             creator: "Paul Sanders".to_string(),
119///             number_of_forms: 1,
120///             forms: Some(vec![Form {
121///                 name: "demographic.form.name.site.demographics".to_string(),
122///                 last_modified: Some(
123///                     DateTime::parse_from_rfc3339("2023-04-15T16:08:19Z")
124///                         .unwrap()
125///                         .with_timezone(&Utc),
126///                 ),
127///                 who_last_modified_name: Some("Paul Sanders".to_string()),
128///                 who_last_modified_role: Some("Project Manager".to_string()),
129///                 when_created: 1681574834930,
130///                 has_errors: false,
131///                 has_warnings: false,
132///                 locked: false,
133///                 user: None,
134///                 date_time_changed: None,
135///                 form_title: "Site Demographics".to_string(),
136///                 form_index: 1,
137///                 form_group: Some("Demographic".to_string()),
138///                 form_state: "In-Work".to_string(),
139///                 lock_state: None,
140///                 states: Some(vec![State {
141///                     value: "form.state.in.work".to_string(),
142///                     signer: "Paul Sanders - Project Manager".to_string(),
143///                     signer_unique_id: "1681162687395".to_string(),
144///                     date_signed: Some(
145///                         DateTime::parse_from_rfc3339("2023-04-15T16:08:19Z")
146///                             .unwrap()
147///                             .with_timezone(&Utc),
148///                     ),
149///                 }]),
150///                 categories: Some(vec![
151///                     Category {
152///                         name: "Demographics".to_string(),
153///                         category_type: "normal".to_string(),
154///                         highest_index: 0,
155///                         fields: Some(vec![
156///                             Field {
157///                                 name: "address".to_string(),
158///                                 field_type: "text".to_string(),
159///                                 data_type: Some("string".to_string()),
160///                                 error_code: "valid".to_string(),
161///                                 when_created: Some(DateTime::parse_from_rfc3339(
162///                                     "2023-04-15T16:07:14Z",
163///                                 )
164///                                 .unwrap()
165///                                 .with_timezone(&Utc)),
166///                                 keep_history: true,
167///                                 entries: None,
168///                                 comments: None,
169///                             },
170///                             Field {
171///                                 name: "company".to_string(),
172///                                 field_type: "text".to_string(),
173///                                 data_type: Some("string".to_string()),
174///                                 error_code: "valid".to_string(),
175///                                 when_created: Some(DateTime::parse_from_rfc3339(
176///                                     "2023-04-15T16:07:14Z",
177///                                 )
178///                                 .unwrap()
179///                                 .with_timezone(&Utc)),
180///                                 keep_history: true,
181///                                 entries: Some(vec![Entry {
182///                                     entry_id: "1".to_string(),
183///                                     value: Some(Value {
184///                                         by: "Paul Sanders".to_string(),
185///                                         by_unique_id: Some("1681162687395".to_string()),
186///                                         role: "Project Manager".to_string(),
187///                                         when: Some(DateTime::parse_from_rfc3339(
188///                                             "2023-04-15T16:08:19Z",
189///                                         )
190///                                         .unwrap()
191///                                         .with_timezone(&Utc)),
192///                                         value: "Some Company".to_string(),
193///                                     }),
194///                                     reason: None,
195///                                 }]),
196///                                 comments: None,
197///                             },
198///                             Field {
199///                                 name: "site_code_name".to_string(),
200///                                 field_type: "hidden".to_string(),
201///                                 data_type: Some("string".to_string()),
202///                                 error_code: "valid".to_string(),
203///                                 when_created: Some(DateTime::parse_from_rfc3339(
204///                                     "2023-04-15T16:07:14Z",
205///                                 )
206///                                 .unwrap()
207///                                 .with_timezone(&Utc)),
208///                                 keep_history: true,
209///                                 entries: Some(vec![
210///                                     Entry {
211///                                         entry_id: "1".to_string(),
212///                                         value: Some(Value {
213///                                             by: "set from calculation".to_string(),
214///                                             by_unique_id: None,
215///                                             role: "System".to_string(),
216///                                             when: Some(DateTime::parse_from_rfc3339(
217///                                                 "2023-04-15T16:08:19Z",
218///                                             )
219///                                             .unwrap()
220///                                             .with_timezone(&Utc)),
221///                                             value: "ABC-Some Site".to_string(),
222///                                         }),
223///                                         reason: Some(Reason {
224///                                             by: "set from calculation".to_string(),
225///                                             by_unique_id: None,
226///                                             role: "System".to_string(),
227///                                             when: Some(DateTime::parse_from_rfc3339(
228///                                                 "2023-04-15T16:08:19Z",
229///                                             )
230///                                             .unwrap()
231///                                             .with_timezone(&Utc)),
232///                                             value: "calculated value".to_string(),
233///                                         }),
234///                                     },
235///                                     Entry {
236///                                         entry_id: "2".to_string(),
237///                                         value: Some(Value {
238///                                             by: "set from calculation".to_string(),
239///                                             by_unique_id: None,
240///                                             role: "System".to_string(),
241///                                             when: Some(DateTime::parse_from_rfc3339(
242///                                                 "2023-04-15T16:07:24Z",
243///                                             )
244///                                             .unwrap()
245///                                             .with_timezone(&Utc)),
246///                                             value: "Some Site".to_string(),
247///                                         }),
248///                                         reason: Some(Reason {
249///                                             by: "set from calculation".to_string(),
250///                                             by_unique_id: None,
251///                                             role: "System".to_string(),
252///                                             when: Some(DateTime::parse_from_rfc3339(
253///                                                 "2023-04-15T16:07:24Z",
254///                                             )
255///                                             .unwrap()
256///                                             .with_timezone(&Utc)),
257///                                             value: "calculated value".to_string(),
258///                                         }),
259///                                     },
260///                                 ]),
261///                                 comments: None,
262///                             },
263///                         ]),
264///                     },
265///                     Category {
266///                         name: "Enrollment".to_string(),
267///                         category_type: "normal".to_string(),
268///                         highest_index: 0,
269///                         fields: Some(vec![
270///                             Field {
271///                                 name: "enrollment_closed_date".to_string(),
272///                                 field_type: "popUpCalendar".to_string(),
273///                                 data_type: Some("date".to_string()),
274///                                 error_code: "valid".to_string(),
275///                                 when_created: Some(DateTime::parse_from_rfc3339(
276///                                     "2023-04-15T16:07:14Z",
277///                                 )
278///                                 .unwrap()
279///                                 .with_timezone(&Utc)),
280///                                 keep_history: true,
281///                                 entries: None,
282///                                 comments: None,
283///                             },
284///                             Field {
285///                                 name: "enrollment_open".to_string(),
286///                                 field_type: "radio".to_string(),
287///                                 data_type: Some("string".to_string()),
288///                                 error_code: "valid".to_string(),
289///                                 when_created: Some(DateTime::parse_from_rfc3339(
290///                                     "2023-04-15T16:07:14Z",
291///                                 )
292///                                 .unwrap()
293///                                 .with_timezone(&Utc)),
294///                                 keep_history: true,
295///                                 entries: Some(vec![Entry {
296///                                     entry_id: "1".to_string(),
297///                                     value: Some(Value {
298///                                         by: "Paul Sanders".to_string(),
299///                                         by_unique_id: Some("1681162687395".to_string()),
300///                                         role: "Project Manager".to_string(),
301///                                         when: Some(DateTime::parse_from_rfc3339(
302///                                             "2023-04-15T16:08:19Z",
303///                                         )
304///                                         .unwrap()
305///                                         .with_timezone(&Utc)),
306///                                         value: "Yes".to_string(),
307///                                     }),
308///                                     reason: None,
309///                                 }]),
310///                                 comments: None,
311///                             },
312///                             Field {
313///                                 name: "enrollment_open_date".to_string(),
314///                                 field_type: "popUpCalendar".to_string(),
315///                                 data_type: Some("date".to_string()),
316///                                 error_code: "valid".to_string(),
317///                                 when_created: Some(DateTime::parse_from_rfc3339(
318///                                     "2023-04-15T16:07:14Z",
319///                                 )
320///                                 .unwrap()
321///                                 .with_timezone(&Utc)),
322///                                 keep_history: true,
323///                                 entries: None,
324///                                 comments: None,
325///                             },
326///                         ]),
327///                     },
328///                 ]),
329///             }]),
330///         },
331///         Site {
332///             name: "Artemis".to_string(),
333///             unique_id: "1691420994591".to_string(),
334///             number_of_patients: 0,
335///             count_of_randomized_patients: 0,
336///             when_created: Some(DateTime::parse_from_rfc3339("2023-08-07T15:14:23Z")
337///                 .unwrap()
338///                 .with_timezone(&Utc)),
339///             creator: "Paul Sanders".to_string(),
340///             number_of_forms: 1,
341///             forms: Some(vec![Form {
342///                 name: "demographic.form.name.site.demographics".to_string(),
343///                 last_modified: Some(
344///                     DateTime::parse_from_rfc3339("2023-08-07T15:14:23Z")
345///                         .unwrap()
346///                         .with_timezone(&Utc),
347///                 ),
348///                 who_last_modified_name: Some("Paul Sanders".to_string()),
349///                 who_last_modified_role: Some("Project Manager".to_string()),
350///                 when_created: 1691420994611,
351///                 has_errors: false,
352///                 has_warnings: false,
353///                 locked: false,
354///                 user: None,
355///                 date_time_changed: None,
356///                 form_title: "Site Demographics".to_string(),
357///                 form_index: 1,
358///                 form_group: Some("Demographic".to_string()),
359///                 form_state: "In-Work".to_string(),
360///                 lock_state: None,
361///                 states: Some(vec![State {
362///                     value: "form.state.in.work".to_string(),
363///                     signer: "Paul Sanders - Project Manager".to_string(),
364///                     signer_unique_id: "1681162687395".to_string(),
365///                     date_signed: Some(
366///                         DateTime::parse_from_rfc3339("2023-08-07T15:14:23Z")
367///                             .unwrap()
368///                             .with_timezone(&Utc),
369///                     ),
370///                 }]),
371///                 categories: Some(vec![Category {
372///                     name: "Demographics".to_string(),
373///                     category_type: "normal".to_string(),
374///                     highest_index: 0,
375///                     fields: Some(vec![Field {
376///                         name: "address".to_string(),
377///                         field_type: "text".to_string(),
378///                         data_type: Some("string".to_string()),
379///                         error_code: "valid".to_string(),
380///                         when_created: Some(DateTime::parse_from_rfc3339("2023-08-07T15:09:54Z")
381///                             .unwrap()
382///                             .with_timezone(&Utc)),
383///                         keep_history: true,
384///                         entries: Some(vec![Entry {
385///                             entry_id: "1".to_string(),
386///                             value: Some(Value {
387///                                 by: "Paul Sanders".to_string(),
388///                                 by_unique_id: Some("1681162687395".to_string()),
389///                                 role: "Project Manager".to_string(),
390///                                 when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:14:21Z")
391///                                     .unwrap()
392///                                     .with_timezone(&Utc)),
393///                                 value: "1111 Moon Drive".to_string(),
394///                             }),
395///                             reason: None,
396///                         }]),
397///                         comments: Some(vec![Comment {
398///                             comment_id: "1".to_string(),
399///                             value: Some(Value {
400///                                 by: "Paul Sanders".to_string(),
401///                                 by_unique_id: Some("1681162687395".to_string()),
402///                                 role: "Project Manager".to_string(),
403///                                 when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:14:21Z")
404///                                     .unwrap()
405///                                     .with_timezone(&Utc)),
406///                                 value: "Some comment".to_string(),
407///                             }),
408///                         }]),
409///                     }]),
410///                 }]),
411///             }]),
412///         },
413///     ],
414/// };
415/// let result = parse_site_native_string(xml).unwrap();
416/// assert_eq!(result, expected);
417pub fn parse_site_native_string(xml_str: &str) -> Result<SiteNative, Error> {
418    let native: SiteNative = quick_xml::de::from_str(xml_str)?;
419
420    Ok(native)
421}
422
423/// Parses a Prelude native subject XML file into a `SubjectNative` stuct.
424///
425/// # Example
426///
427/// ```
428/// use std::path::Path;
429///
430/// use prelude_xml_parser::parse_subject_native_file;
431///
432/// let file_path = Path::new("tests/assets/subject_native.xml");
433/// let native = parse_subject_native_file(&file_path).unwrap();
434///
435/// assert!(native.patients.len() >= 1, "Vector length is less than 1");
436/// ```
437pub fn parse_subject_native_file(xml_path: &Path) -> Result<SubjectNative, Error> {
438    check_valid_xml_file(xml_path)?;
439
440    let file = File::open(xml_path)?;
441    let buf_reader = BufReader::new(file);
442    parse_subject_native_streaming(buf_reader)
443}
444
445/// Parse a string of Prelude native subject XML into a `SubjectNative` struct.
446///
447/// # Example
448///
449/// ```
450/// use chrono::{DateTime, Utc};
451/// use prelude_xml_parser::parse_subject_native_string;
452/// use prelude_xml_parser::native::common::LockState;
453/// use prelude_xml_parser::native::subject_native::*;
454///
455/// let xml = r#"<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Paul Sanders" role="Project Manager" numberSubjectsProcessed="4">
456///     <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">
457///       <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">
458///         <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-04-15 12:09:02 -0400"/>
459///         <category name="Demographics" type="normal" highestIndex="0">
460///           <field name="breed" type="combo-box" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
461///             <entry id="1">
462///               <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Labrador</value>
463///             </entry>
464///           </field>
465///         </category>
466///       </form>
467///     </patient>
468///     <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">
469///       <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">
470///         <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-04-16 12:10:02 -0400"/>
471///         <category name="Demographics" type="normal" highestIndex="0">
472///           <field name="breed" type="combo-box" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
473///             <entry id="1">
474///               <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Labrador</value>
475///             </entry>
476///           </field>
477///         </category>
478///       </form>
479///     </patient>
480/// </export_from_vision_EDC>
481/// "#;
482///
483/// let expected = SubjectNative {
484///     patients: vec![
485///         Patient {
486///             patient_id: "ABC-001".to_string(),
487///             unique_id: "1681574905819".to_string(),
488///             when_created: Some(DateTime::parse_from_rfc3339("2023-04-15T16:09:02Z")
489///                 .unwrap()
490///                 .with_timezone(&Utc)),
491///             creator: "Paul Sanders".to_string(),
492///             site_name: "Some Site".to_string(),
493///             site_unique_id: "1681574834910".to_string(),
494///             last_language: Some("English".to_string()),
495///             number_of_forms: 6,
496///             forms: Some(vec![Form {
497///                 name: "day.0.form.name.demographics".to_string(),
498///                 last_modified: Some(DateTime::parse_from_rfc3339("2023-04-15T16:09:15Z")
499///                     .unwrap()
500///                     .with_timezone(&Utc)),
501///                 who_last_modified_name: Some("Paul Sanders".to_string()),
502///                 who_last_modified_role: Some("Project Manager".to_string()),
503///                 when_created: 1681574905839,
504///                 has_errors: false,
505///                 has_warnings: false,
506///                 locked: false,
507///                 user: None,
508///                 date_time_changed: None,
509///                 form_title: "Demographics".to_string(),
510///                 form_index: 1,
511///                 form_group: Some("Day 0".to_string()),
512///                 form_state: "In-Work".to_string(),
513///                 lock_state: None,
514///                 states: Some(vec![State {
515///                     value: "form.state.in.work".to_string(),
516///                     signer: "Paul Sanders - Project Manager".to_string(),
517///                     signer_unique_id: "1681162687395".to_string(),
518///                     date_signed: Some(
519///                         DateTime::parse_from_rfc3339("2023-04-15T16:09:02Z")
520///                             .unwrap()
521///                             .with_timezone(&Utc),
522///                     ),
523///                 }]),
524///                 categories: Some(vec![Category {
525///                     name: "Demographics".to_string(),
526///                     category_type: "normal".to_string(),
527///                     highest_index: 0,
528///                     fields: Some(vec![Field {
529///                         name: "breed".to_string(),
530///                         field_type: "combo-box".to_string(),
531///                         data_type: Some("string".to_string()),
532///                         error_code: "valid".to_string(),
533///                         when_created: Some(DateTime::parse_from_rfc3339("2023-04-15T16:08:26Z")
534///                             .unwrap()
535///                             .with_timezone(&Utc)),
536///                         keep_history: true,
537///                         entries: Some(vec![Entry {
538///                             entry_id: "1".to_string(),
539///                             value: Some(Value {
540///                                 by: "Paul Sanders".to_string(),
541///                                 by_unique_id: Some("1681162687395".to_string()),
542///                                 role: "Project Manager".to_string(),
543///                                 when: Some(DateTime::parse_from_rfc3339("2023-04-15T16:09:02Z")
544///                                     .unwrap()
545///                                     .with_timezone(&Utc)),
546///                                 value: "Labrador".to_string(),
547///                             }),
548///                             reason: None,
549///                         }]),
550///                         comments: None,
551///                     }]),
552///                 }]),
553///             }]),
554///         },
555///         Patient {
556///             patient_id: "DEF-002".to_string(),
557///             unique_id: "1681574905820".to_string(),
558///             when_created: Some(DateTime::parse_from_rfc3339("2023-04-16T16:10:02Z")
559///                 .unwrap()
560///                 .with_timezone(&Utc)),
561///             creator: "Wade Watts".to_string(),
562///             site_name: "Another Site".to_string(),
563///             site_unique_id: "1681574834911".to_string(),
564///             last_language: None,
565///             number_of_forms: 8,
566///             forms: Some(vec![Form {
567///                 name: "day.0.form.name.demographics".to_string(),
568///                 last_modified: Some(DateTime::parse_from_rfc3339("2023-04-16T16:10:15Z")
569///                     .unwrap()
570///                     .with_timezone(&Utc)),
571///                 who_last_modified_name: Some("Barney Rubble".to_string()),
572///                 who_last_modified_role: Some("Technician".to_string()),
573///                 when_created: 1681574905838,
574///                 has_errors: false,
575///                 has_warnings: false,
576///                 locked: false,
577///                 user: None,
578///                 date_time_changed: None,
579///                 form_title: "Demographics".to_string(),
580///                 form_index: 1,
581///                 form_group: Some("Day 0".to_string()),
582///                 form_state: "In-Work".to_string(),
583///                 lock_state: None,
584///                 states: Some(vec![State {
585///                     value: "form.state.in.work".to_string(),
586///                     signer: "Paul Sanders - Project Manager".to_string(),
587///                     signer_unique_id: "1681162687395".to_string(),
588///                     date_signed: Some(
589///                         DateTime::parse_from_rfc3339("2023-04-16T16:10:02Z")
590///                             .unwrap()
591///                             .with_timezone(&Utc),
592///                     ),
593///                 }]),
594///                 categories: Some(vec![Category {
595///                     name: "Demographics".to_string(),
596///                     category_type: "normal".to_string(),
597///                     highest_index: 0,
598///                     fields: Some(vec![Field {
599///                         name: "breed".to_string(),
600///                         field_type: "combo-box".to_string(),
601///                         data_type: Some("string".to_string()),
602///                         error_code: "valid".to_string(),
603///                         when_created: Some(DateTime::parse_from_rfc3339("2023-04-15T16:08:26Z")
604///                             .unwrap()
605///                             .with_timezone(&Utc)),
606///                         keep_history: true,
607///                         entries: Some(vec![Entry {
608///                             entry_id: "1".to_string(),
609///                             value: Some(Value {
610///                                 by: "Paul Sanders".to_string(),
611///                                 by_unique_id: Some("1681162687395".to_string()),
612///                                 role: "Project Manager".to_string(),
613///                                 when: Some(DateTime::parse_from_rfc3339("2023-04-15T16:09:02Z")
614///                                     .unwrap()
615///                                     .with_timezone(&Utc)),
616///                                 value: "Labrador".to_string(),
617///                             }),
618///                             reason: None,
619///                         }]),
620///                         comments: None,
621///                     }]),
622///                 }]),
623///             }]),
624///         },
625///     ],
626/// };
627/// let result = parse_subject_native_string(xml).unwrap();
628///
629/// assert_eq!(result, expected);
630/// ```
631pub fn parse_subject_native_string(xml_str: &str) -> Result<SubjectNative, Error> {
632    parse_subject_native_streaming(Cursor::new(xml_str.as_bytes()))
633}
634
635fn parse_subject_native_streaming<R: std::io::BufRead>(reader: R) -> Result<SubjectNative, Error> {
636    let mut xml_reader = Reader::from_reader(reader);
637    xml_reader.config_mut().trim_text(true);
638
639    let mut patients = Vec::new();
640    let mut buf = Vec::new();
641    let mut text_content = String::new();
642
643    let mut current_patient: Option<Patient> = None;
644    let mut current_forms: Vec<Form> = Vec::new();
645    let mut current_form: Option<Form> = None;
646    let mut current_states: Vec<State> = Vec::new();
647    let mut current_categories: Vec<Category> = Vec::new();
648    let mut current_category: Option<Category> = None;
649    let mut current_fields: Vec<Field> = Vec::new();
650    let mut current_field: Option<Field> = None;
651    let mut current_entries: Vec<Entry> = Vec::new();
652    let mut current_entry: Option<Entry> = None;
653    let mut current_comments: Vec<Comment> = Vec::new();
654    let mut current_comment: Option<Comment> = None;
655    let mut current_value: Option<Value> = None;
656    let mut current_reason: Option<Reason> = None;
657
658    let mut in_patient = false;
659    let mut in_form = false;
660    let mut in_category = false;
661    let mut in_field = false;
662    let mut in_entry = false;
663    let mut in_comment = false;
664    let mut in_value = false;
665    let mut in_reason = false;
666
667    loop {
668        match xml_reader.read_event_into(&mut buf) {
669            Err(e) => {
670                return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
671                    format!("XML reading error: {}", e),
672                )))
673            }
674            Ok(Event::Eof) => break,
675
676            Ok(Event::Start(ref e)) => {
677                let name_bytes = e.local_name();
678                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
679                    match name {
680                        "patient" => {
681                            let attrs = extract_attributes(e)?;
682                            current_patient = Some(Patient::from_attributes(attrs)?);
683                            in_patient = true;
684                            current_forms.clear();
685                        }
686                        "form" if in_patient => {
687                            let attrs = extract_attributes(e)?;
688                            current_form = Some(Form::from_attributes(attrs)?);
689                            in_form = true;
690                            current_states.clear();
691                            current_categories.clear();
692                        }
693                        "category" if in_form => {
694                            let attrs = extract_attributes(e)?;
695                            current_category = Some(Category::from_attributes(attrs)?);
696                            in_category = true;
697                            current_fields.clear();
698                        }
699                        "field" if in_category => {
700                            let attrs = extract_attributes(e)?;
701                            current_field = Some(Field::from_attributes(attrs)?);
702                            in_field = true;
703                            current_entries.clear();
704                            current_comments.clear();
705                        }
706                        "entry" if in_field => {
707                            let attrs = extract_attributes(e)?;
708                            current_entry = Some(Entry::from_attributes(attrs)?);
709                            in_entry = true;
710                        }
711                        "comment" if in_field => {
712                            let attrs = extract_attributes(e)?;
713                            let comment_id = attrs.get("id").cloned().unwrap_or_default();
714                            current_comment = Some(Comment {
715                                comment_id,
716                                value: None,
717                            });
718                            in_comment = true;
719                        }
720                        "value" if in_entry || in_comment => {
721                            let attrs = extract_attributes(e)?;
722                            current_value = Some(Value::from_attributes(attrs)?);
723                            in_value = true;
724                            text_content.clear();
725                        }
726                        "reason" if in_entry => {
727                            let attrs = extract_attributes(e)?;
728                            current_reason = Some(Reason::from_attributes(attrs)?);
729                            in_reason = true;
730                            text_content.clear();
731                        }
732                        _ => {}
733                    }
734                }
735            }
736
737            Ok(Event::Text(e)) => {
738                if in_value || in_reason {
739                    text_content.push_str(&String::from_utf8_lossy(&e));
740                }
741            }
742
743            Ok(Event::End(ref e)) => {
744                let name_bytes = e.local_name();
745                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
746                    match name {
747                        "patient" => {
748                            if let Some(mut patient) = current_patient.take() {
749                                if !current_forms.is_empty() {
750                                    patient.set_forms(current_forms.clone());
751                                }
752                                patients.push(patient);
753                            }
754                            in_patient = false;
755                            current_forms.clear();
756                        }
757                        "form" if in_form => {
758                            if let Some(mut form) = current_form.take() {
759                                if !current_states.is_empty() {
760                                    form.states = Some(current_states.clone());
761                                }
762                                if !current_categories.is_empty() {
763                                    form.categories = Some(current_categories.clone());
764                                }
765                                current_forms.push(form);
766                            }
767                            in_form = false;
768                            current_states.clear();
769                            current_categories.clear();
770                        }
771                        "category" if in_category => {
772                            if let Some(mut category) = current_category.take() {
773                                if !current_fields.is_empty() {
774                                    category.fields = Some(current_fields.clone());
775                                }
776                                current_categories.push(category);
777                            }
778                            in_category = false;
779                            current_fields.clear();
780                        }
781                        "field" if in_field => {
782                            if let Some(mut field) = current_field.take() {
783                                if !current_entries.is_empty() {
784                                    field.entries = Some(current_entries.clone());
785                                }
786                                if !current_comments.is_empty() {
787                                    field.comments = Some(current_comments.clone());
788                                }
789                                current_fields.push(field);
790                            }
791                            in_field = false;
792                            current_entries.clear();
793                            current_comments.clear();
794                        }
795                        "entry" if in_entry => {
796                            if let Some(entry) = current_entry.take() {
797                                current_entries.push(entry);
798                            }
799                            in_entry = false;
800                        }
801                        "comment" if in_comment => {
802                            if let Some(comment) = current_comment.take() {
803                                current_comments.push(comment);
804                            }
805                            in_comment = false;
806                        }
807                        "value" if in_value => {
808                            if let Some(mut value) = current_value.take() {
809                                value.value = text_content.clone();
810                                if let Some(ref mut entry) = current_entry {
811                                    entry.value = Some(value.clone());
812                                }
813                                if let Some(ref mut comment) = current_comment {
814                                    comment.value = Some(value);
815                                }
816                            }
817                            in_value = false;
818                            text_content.clear();
819                        }
820                        "reason" if in_reason => {
821                            if let Some(mut reason) = current_reason.take() {
822                                reason.value = text_content.clone();
823                                if let Some(ref mut entry) = current_entry {
824                                    entry.reason = Some(reason);
825                                }
826                            }
827                            in_reason = false;
828                            text_content.clear();
829                        }
830                        _ => {}
831                    }
832                }
833            }
834
835            Ok(Event::Empty(ref e)) => {
836                let name_bytes = e.local_name();
837                if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
838                    match name {
839                        "state" if in_form => {
840                            let attrs = extract_attributes(e)?;
841                            let state = State::from_attributes(attrs)?;
842                            current_states.push(state);
843                        }
844                        "lockState" if in_form => {
845                            let attrs = extract_attributes(e)?;
846                            let lock_state = LockState::from_attributes(attrs)?;
847                            if let Some(ref mut form) = current_form {
848                                form.lock_state = Some(lock_state);
849                            }
850                        }
851                        "value" if in_entry => {
852                            let attrs = extract_attributes(e)?;
853                            let value = Value::from_attributes(attrs)?;
854                            if let Some(ref mut entry) = current_entry {
855                                entry.value = Some(value);
856                            }
857                        }
858                        "reason" if in_entry => {
859                            let attrs = extract_attributes(e)?;
860                            let reason = Reason::from_attributes(attrs)?;
861                            if let Some(ref mut entry) = current_entry {
862                                entry.reason = Some(reason);
863                            }
864                        }
865                        _ => {}
866                    }
867                }
868            }
869
870            _ => {}
871        }
872
873        buf.clear();
874    }
875
876    Ok(SubjectNative { patients })
877}
878
879fn extract_attributes(e: &BytesStart) -> Result<HashMap<String, String>, Error> {
880    let mut attrs = HashMap::new();
881    for attr in e.attributes() {
882        let attr = attr.map_err(|e| {
883            Error::ParsingError(quick_xml::de::DeError::Custom(format!(
884                "Attribute error: {}",
885                e
886            )))
887        })?;
888        let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
889        let value = String::from_utf8_lossy(&attr.value).to_string();
890        attrs.insert(key, value);
891    }
892    Ok(attrs)
893}
894
895/// Parses a Prelude native user XML file into a `UserNative` stuct.
896///
897/// # Example
898///
899/// ```
900/// use std::path::Path;
901///
902/// use prelude_xml_parser::parse_user_native_file;
903///
904/// let file_path = Path::new("tests/assets/user_native.xml");
905/// let native = parse_user_native_file(&file_path).unwrap();
906///
907/// assert!(native.users.len() >= 1, "Vector length is less than 1");
908/// ```
909pub fn parse_user_native_file(xml_path: &Path) -> Result<UserNative, Error> {
910    check_valid_xml_file(xml_path)?;
911
912    let xml_file = read_to_string(xml_path)?;
913    let native = parse_user_native_string(&xml_file)?;
914
915    Ok(native)
916}
917
918/// Parse a string of Prelude native user XML into a `UserNative` struct.
919///
920/// # Example
921///
922/// ```
923/// use chrono::{DateTime, Utc};
924/// use prelude_xml_parser::parse_user_native_string;
925/// use prelude_xml_parser::native::user_native::*;
926///
927/// let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
928///   <export_from_vision_EDC date="02-Jun-2024 06:59 -0500" createdBy="Paul Sanders" role="Project Manager" numberSubjectsProcessed="3">
929///     <user uniqueId="1691421275437" lastLanguage="" creator="Paul Sanders(1681162687395)" numberOfForms="1">
930///       <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">
931///         <state value="form.state.in.work" signer="Paul Sanders - Project Manager" signerUniqueId="1681162687395" dateSigned="2023-08-07 10:15:41 -0500" />
932///         <category name="demographics" type="normal" highestIndex="0">
933///           <field name="address" type="text" dataType="string" errorCode="undefined" whenCreated="2024-01-12 14:14:09 -0600" keepHistory="true" />
934///           <field name="email" type="text" dataType="string" errorCode="undefined" whenCreated="2023-08-07 10:15:41 -0500" keepHistory="true">
935///             <entry id="1">
936///               <value by="Paul Sanders" byUniqueId="1681162687395" role="Project Manager" when="2023-08-07 10:15:41 -0500" xml:space="preserve">jazz@artemis.com</value>
937///             </entry>
938///           </field>
939///         </category>
940///         <category name="Administrative" type="normal" highestIndex="0">
941///           <field name="study_assignment" type="text" dataType="" errorCode="undefined" whenCreated="2023-08-07 10:15:41 -0500" keepHistory="true">
942///             <entry id="1">
943///               <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>
944///               <reason by="set from calculation" byUniqueId="" role="System" when="2023-08-07 10:15:41 -0500" xml:space="preserve">calculated value</reason>
945///             </entry>
946///           </field>
947///         </category>
948///       </form>
949///     </user>
950///   </export_from_vision_EDC>
951/// "#;
952///
953/// let expected = UserNative {
954///     users: vec![User {
955///         unique_id: "1691421275437".to_string(),
956///         last_language: None,
957///         creator: "Paul Sanders(1681162687395)".to_string(),
958///         number_of_forms: 1,
959///         forms: Some(vec![Form {
960///             name: "form.name.demographics".to_string(),
961///             last_modified: Some(
962///                 DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
963///                     .unwrap()
964///                     .with_timezone(&Utc),
965///             ),
966///             who_last_modified_name: Some("Paul Sanders".to_string()),
967///             who_last_modified_role: Some("Project Manager".to_string()),
968///             when_created: 1691421341578,
969///             has_errors: false,
970///             has_warnings: false,
971///             locked: false,
972///             user: None,
973///             date_time_changed: None,
974///             form_title: "User Demographics".to_string(),
975///             form_index: 1,
976///             form_group: None,
977///             form_state: "In-Work".to_string(),
978///             lock_state: None,
979///             states: Some(vec![State {
980///                 value: "form.state.in.work".to_string(),
981///                 signer: "Paul Sanders - Project Manager".to_string(),
982///                 signer_unique_id: "1681162687395".to_string(),
983///                 date_signed: Some(
984///                     DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
985///                         .unwrap()
986///                         .with_timezone(&Utc),
987///                 ),
988///             }]),
989///             categories: Some(vec![
990///                         Category {
991///                             name: "demographics".to_string(),
992///                             category_type: "normal".to_string(),
993///                             highest_index: 0,
994///                             fields: Some(vec![
995///                                 Field {
996///                                     name: "address".to_string(),
997///                                     field_type: "text".to_string(),
998///                                     data_type: Some("string".to_string()),
999///                                     error_code: "undefined".to_string(),
1000///                                     when_created: Some(DateTime::parse_from_rfc3339("2024-01-12T20:14:09Z")
1001///                                         .unwrap()
1002///                                         .with_timezone(&Utc)),
1003///                                     keep_history: true,
1004///                                     entries: None,
1005///                                     comments: None,
1006///                                 },
1007///                                 Field {
1008///                                     name: "email".to_string(),
1009///                                     field_type: "text".to_string(),
1010///                                     data_type: Some("string".to_string()),
1011///                                     error_code: "undefined".to_string(),
1012///                                     when_created: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1013///                                         .unwrap()
1014///                                         .with_timezone(&Utc)),
1015///                                     keep_history: true,
1016///                                     entries: Some(vec![Entry {
1017///                                         entry_id: "1".to_string(),
1018///                                         value: Some(Value {
1019///                                             by: "Paul Sanders".to_string(),
1020///                                             by_unique_id: Some("1681162687395".to_string()),
1021///                                             role: "Project Manager".to_string(),
1022///                                             when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1023///                                                 .unwrap()
1024///                                                 .with_timezone(&Utc)),
1025///                                             value: "jazz@artemis.com".to_string(),
1026///                                         }),
1027///                                         reason: None,
1028///                                     }]),
1029///                                     comments: None,
1030///                                 },
1031///                             ]),
1032///                         },
1033///                         Category {
1034///                             name: "Administrative".to_string(),
1035///                             category_type: "normal".to_string(),
1036///                             highest_index: 0,
1037///                             fields: Some(vec![
1038///                                 Field {
1039///                                     name: "study_assignment".to_string(),
1040///                                     field_type: "text".to_string(),
1041///                                     data_type: None,
1042///                                     error_code: "undefined".to_string(),
1043///                                     when_created: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1044///                                         .unwrap()
1045///                                         .with_timezone(&Utc)),
1046///                                     keep_history: true,
1047///                                     entries: Some(vec![
1048///                                         Entry {
1049///                                             entry_id: "1".to_string(),
1050///                                             value: Some(Value {
1051///                                                 by: "set from calculation".to_string(),
1052///                                                 by_unique_id: None,
1053///                                                 role: "System".to_string(),
1054///                                                 when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1055///                                                     .unwrap()
1056///                                                     .with_timezone(&Utc)),
1057///                                                 value: "On 07-Aug-2023 10:15 -0500, Paul Sanders assigned user from another study".to_string(),
1058///                                             }),
1059///                                             reason: Some(Reason {
1060///                                                 by: "set from calculation".to_string(),
1061///                                                 by_unique_id: None,
1062///                                                 role: "System".to_string(),
1063///                                                 when: Some(DateTime::parse_from_rfc3339("2023-08-07T15:15:41Z")
1064///                                                     .unwrap()
1065///                                                     .with_timezone(&Utc)),
1066///                                                 value: "calculated value".to_string(),
1067///                                             }),
1068///                                         },
1069///                                     ]),
1070///                                     comments: None,
1071///                                 },
1072///                             ]),
1073///                         },
1074///             ]),
1075///         }]),
1076///     }],
1077/// };
1078///
1079/// let result = parse_user_native_string(xml).unwrap();
1080///
1081/// assert_eq!(result, expected);
1082/// ```
1083pub fn parse_user_native_string(xml_str: &str) -> Result<UserNative, Error> {
1084    let native: UserNative = quick_xml::de::from_str(xml_str)?;
1085
1086    Ok(native)
1087}
1088
1089fn check_valid_xml_file(xml_path: &Path) -> Result<(), Error> {
1090    if !xml_path.exists() {
1091        return Err(Error::FileNotFound(xml_path.to_path_buf()));
1092    }
1093
1094    if let Some(extension) = xml_path.extension() {
1095        if extension != "xml" {
1096            return Err(Error::InvalidFileType(xml_path.to_owned()));
1097        }
1098    } else {
1099        return Err(Error::Unknown);
1100    }
1101
1102    Ok(())
1103}
1104
1105#[cfg(test)]
1106mod tests {
1107    use super::*;
1108    use tempfile::{tempdir, Builder};
1109
1110    #[test]
1111    fn test_site_file_not_found_error() {
1112        let dir = tempdir().unwrap().path().to_path_buf();
1113        let result = parse_site_native_file(&dir);
1114        assert!(result.is_err());
1115        assert!(matches!(result, Err(Error::FileNotFound(_))));
1116    }
1117
1118    #[test]
1119    fn test_site_invaid_file_type_error() {
1120        let file = Builder::new()
1121            .prefix("test")
1122            .suffix(".csv")
1123            .tempfile()
1124            .unwrap();
1125        let result = parse_site_native_file(file.path());
1126
1127        assert!(result.is_err());
1128        assert!(matches!(result, Err(Error::InvalidFileType(_))));
1129    }
1130
1131    #[test]
1132    fn test_subject_file_not_found_error() {
1133        let dir = tempdir().unwrap().path().to_path_buf();
1134        let result = parse_subject_native_file(&dir);
1135        assert!(result.is_err());
1136        assert!(matches!(result, Err(Error::FileNotFound(_))));
1137    }
1138
1139    #[test]
1140    fn test_subject_invaid_file_type_error() {
1141        let file = Builder::new()
1142            .prefix("test")
1143            .suffix(".csv")
1144            .tempfile()
1145            .unwrap();
1146        let result = parse_subject_native_file(file.path());
1147
1148        assert!(result.is_err());
1149        assert!(matches!(result, Err(Error::InvalidFileType(_))));
1150    }
1151
1152    #[test]
1153    fn test_user_file_not_found_error() {
1154        let dir = tempdir().unwrap().path().to_path_buf();
1155        let result = parse_user_native_file(&dir);
1156        assert!(result.is_err());
1157        assert!(matches!(result, Err(Error::FileNotFound(_))));
1158    }
1159
1160    #[test]
1161    fn test_user_invaid_file_type_error() {
1162        let file = Builder::new()
1163            .prefix("test")
1164            .suffix(".csv")
1165            .tempfile()
1166            .unwrap();
1167        let result = parse_user_native_file(file.path());
1168
1169        assert!(result.is_err());
1170        assert!(matches!(result, Err(Error::InvalidFileType(_))));
1171    }
1172
1173    #[test]
1174    fn test_forms_parsing_regression() {
1175        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1176<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1177    <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">
1178        <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">
1179            <state value="form.state.in.work" signer="Test User - Tester" signerUniqueId="111111111" dateSigned="2023-04-15 12:09:02 -0400"/>
1180            <category name="Test Category" type="normal" highestIndex="0">
1181                <field name="test_field" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
1182                    <entry id="1">
1183                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Test Value</value>
1184                    </entry>
1185                </field>
1186            </category>
1187        </form>
1188        <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">
1189            <state value="form.state.complete" signer="Test User - Tester" signerUniqueId="111111111" dateSigned="2023-04-15 12:10:02 -0400"/>
1190        </form>
1191    </patient>
1192</export_from_vision_EDC>"#;
1193
1194        let result = parse_subject_native_string(xml).expect("Should parse successfully");
1195
1196        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1197
1198        let patient = &result.patients[0];
1199        assert_eq!(patient.patient_id, "TEST-001");
1200        assert_eq!(patient.number_of_forms, 2);
1201
1202        let forms = patient.forms.as_ref().expect("Patient should have forms");
1203        assert_eq!(forms.len(), 2, "Patient should have exactly 2 forms");
1204
1205        let form1 = &forms[0];
1206        assert_eq!(form1.name, "test.form.1");
1207        assert_eq!(form1.form_title, "Test Form 1");
1208        assert_eq!(form1.form_index, 1);
1209        assert_eq!(form1.form_state, "In-Work");
1210
1211        let states1 = form1.states.as_ref().expect("Form 1 should have states");
1212        assert_eq!(states1.len(), 1);
1213        assert_eq!(states1[0].value, "form.state.in.work");
1214
1215        let categories1 = form1
1216            .categories
1217            .as_ref()
1218            .expect("Form 1 should have categories");
1219        assert_eq!(categories1.len(), 1);
1220        assert_eq!(categories1[0].name, "Test Category");
1221
1222        let fields1 = categories1[0]
1223            .fields
1224            .as_ref()
1225            .expect("Category should have fields");
1226        assert_eq!(fields1.len(), 1);
1227        assert_eq!(fields1[0].name, "test_field");
1228
1229        let entries1 = fields1[0]
1230            .entries
1231            .as_ref()
1232            .expect("Field should have entries");
1233        assert_eq!(entries1.len(), 1);
1234        assert_eq!(entries1[0].entry_id, "1");
1235
1236        let value1 = entries1[0].value.as_ref().expect("Entry should have value");
1237        assert_eq!(value1.value, "Test Value");
1238        assert_eq!(value1.by, "Test User");
1239        assert_eq!(value1.role, "Tester");
1240
1241        let form2 = &forms[1];
1242        assert_eq!(form2.name, "test.form.2");
1243        assert_eq!(form2.form_title, "Test Form 2");
1244        assert_eq!(form2.form_index, 2);
1245        assert_eq!(form2.form_state, "Complete");
1246
1247        let states2 = form2.states.as_ref().expect("Form 2 should have states");
1248        assert_eq!(states2.len(), 1);
1249        assert_eq!(states2[0].value, "form.state.complete");
1250    }
1251
1252    #[test]
1253    fn test_comments_parsing_regression() {
1254        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1255<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1256    <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">
1257        <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">
1258            <category name="Test Category" type="normal" highestIndex="0">
1259                <field name="field_with_comments" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
1260                    <entry id="1">
1261                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Field Value</value>
1262                    </entry>
1263                    <comment id="1">
1264                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:05 -0400" xml:space="preserve">First comment</value>
1265                    </comment>
1266                    <comment id="2">
1267                        <value by="Another User" byUniqueId="222222222" role="Reviewer" when="2023-04-15 12:10:00 -0400" xml:space="preserve">Second comment</value>
1268                    </comment>
1269                </field>
1270                <field name="field_without_comments" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:30 -0400" keepHistory="true">
1271                    <entry id="1">
1272                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:10 -0400" xml:space="preserve">Another Value</value>
1273                    </entry>
1274                </field>
1275            </category>
1276        </form>
1277    </patient>
1278</export_from_vision_EDC>"#;
1279
1280        let result = parse_subject_native_string(xml).expect("Should parse successfully");
1281
1282        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1283
1284        let patient = &result.patients[0];
1285        let forms = patient.forms.as_ref().expect("Patient should have forms");
1286        let form = &forms[0];
1287        let categories = form
1288            .categories
1289            .as_ref()
1290            .expect("Form should have categories");
1291        let fields = categories[0]
1292            .fields
1293            .as_ref()
1294            .expect("Category should have fields");
1295        assert_eq!(fields.len(), 2, "Should have 2 fields");
1296
1297        let field_with_comments = &fields[0];
1298        assert_eq!(field_with_comments.name, "field_with_comments");
1299
1300        let comments = field_with_comments
1301            .comments
1302            .as_ref()
1303            .expect("Field should have comments");
1304        assert_eq!(comments.len(), 2, "Should have exactly 2 comments");
1305
1306        let comment1 = &comments[0];
1307        assert_eq!(comment1.comment_id, "1");
1308        let comment1_value = comment1
1309            .value
1310            .as_ref()
1311            .expect("Comment 1 should have value");
1312        assert_eq!(comment1_value.value, "First comment");
1313        assert_eq!(comment1_value.by, "Test User");
1314        assert_eq!(comment1_value.role, "Tester");
1315
1316        let comment2 = &comments[1];
1317        assert_eq!(comment2.comment_id, "2");
1318        let comment2_value = comment2
1319            .value
1320            .as_ref()
1321            .expect("Comment 2 should have value");
1322        assert_eq!(comment2_value.value, "Second comment");
1323        assert_eq!(comment2_value.by, "Another User");
1324        assert_eq!(comment2_value.role, "Reviewer");
1325
1326        let field_without_comments = &fields[1];
1327        assert_eq!(field_without_comments.name, "field_without_comments");
1328        assert!(
1329            field_without_comments.comments.is_none(),
1330            "Field without comments should have no comments"
1331        );
1332    }
1333
1334    #[test]
1335    fn test_empty_forms_handling() {
1336        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1337<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1338    <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">
1339    </patient>
1340</export_from_vision_EDC>"#;
1341
1342        let result = parse_subject_native_string(xml).expect("Should parse successfully");
1343
1344        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1345
1346        let patient = &result.patients[0];
1347        assert_eq!(patient.patient_id, "TEST-003");
1348        assert_eq!(patient.number_of_forms, 0);
1349        assert!(
1350            patient.forms.is_none(),
1351            "Patient with 0 forms should have None for forms"
1352        );
1353    }
1354
1355    #[test]
1356    fn test_large_patient_forms_regression() {
1357        let mut xml = String::from(
1358            r#"<?xml version="1.0" encoding="UTF-8"?>
1359<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1360    <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">"#,
1361        );
1362
1363        for i in 1..=50 {
1364            xml.push_str(&format!(r#"
1365        <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">
1366            <state value="form.state.in.work" signer="Test User - Tester" signerUniqueId="111111111" dateSigned="2023-04-15 12:09:02 -0400"/>
1367            <category name="Category {}" type="normal" highestIndex="0">
1368                <field name="field_{}" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
1369                    <entry id="1">
1370                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Value {}</value>
1371                    </entry>
1372                    <comment id="1">
1373                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:05 -0400" xml:space="preserve">Comment for form {}</value>
1374                    </comment>
1375                </field>
1376            </category>
1377        </form>"#, i, i, i, i, i, i, i, i));
1378        }
1379
1380        xml.push_str(
1381            r#"
1382    </patient>
1383</export_from_vision_EDC>"#,
1384        );
1385
1386        let result =
1387            parse_subject_native_string(&xml).expect("Should parse large patient successfully");
1388
1389        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1390
1391        let patient = &result.patients[0];
1392        assert_eq!(patient.patient_id, "LARGE-TEST");
1393        assert_eq!(patient.number_of_forms, 50);
1394
1395        let forms = patient.forms.as_ref().expect("Patient should have forms");
1396        assert_eq!(forms.len(), 50, "Patient should have exactly 50 forms");
1397
1398        for (i, form) in forms.iter().enumerate() {
1399            let form_num = i + 1;
1400            assert_eq!(form.name, format!("test.form.{}", form_num));
1401            assert_eq!(form.form_title, format!("Test Form {}", form_num));
1402            assert_eq!(form.form_index, form_num);
1403
1404            let categories = form
1405                .categories
1406                .as_ref()
1407                .expect("Form should have categories");
1408            assert_eq!(categories.len(), 1);
1409
1410            let fields = categories[0]
1411                .fields
1412                .as_ref()
1413                .expect("Category should have fields");
1414            assert_eq!(fields.len(), 1);
1415
1416            let entries = fields[0]
1417                .entries
1418                .as_ref()
1419                .expect("Field should have entries");
1420            assert_eq!(entries.len(), 1);
1421            assert_eq!(
1422                entries[0].value.as_ref().unwrap().value,
1423                format!("Value {}", form_num)
1424            );
1425
1426            let comments = fields[0]
1427                .comments
1428                .as_ref()
1429                .expect("Field should have comments");
1430            assert_eq!(comments.len(), 1);
1431            assert_eq!(
1432                comments[0].value.as_ref().unwrap().value,
1433                format!("Comment for form {}", form_num)
1434            );
1435        }
1436    }
1437
1438    #[test]
1439    fn test_malformed_datetime_handling() {
1440        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1441<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1442    <patient patientId="TEST-004" uniqueId="123456793" whenCreated="" creator="Test User" siteName="Test Site" siteUniqueId="987654321" lastLanguage="English" numberOfForms="1">
1443        <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">
1444            <category name="Test Category" type="normal" highestIndex="0">
1445                <field name="test_field" type="text" dataType="string" errorCode="valid" whenCreated="" keepHistory="true">
1446                    <entry id="1">
1447                        <value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Test Value</value>
1448                    </entry>
1449                </field>
1450            </category>
1451        </form>
1452    </patient>
1453</export_from_vision_EDC>"#;
1454
1455        let result =
1456            parse_subject_native_string(xml).expect("Should handle malformed datetimes gracefully");
1457
1458        assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
1459
1460        let patient = &result.patients[0];
1461        assert!(
1462            patient.when_created.is_none(),
1463            "Empty whenCreated should be None"
1464        );
1465
1466        let forms = patient.forms.as_ref().expect("Patient should have forms");
1467        let form = &forms[0];
1468        assert!(
1469            form.last_modified.is_none(),
1470            "Empty lastModified should be None"
1471        );
1472
1473        let categories = form
1474            .categories
1475            .as_ref()
1476            .expect("Form should have categories");
1477        let fields = categories[0]
1478            .fields
1479            .as_ref()
1480            .expect("Category should have fields");
1481        let field = &fields[0];
1482        assert!(
1483            field.when_created.is_none(),
1484            "Empty whenCreated in field should be None"
1485        );
1486    }
1487
1488    #[test]
1489    fn test_empty_datetime_in_value_and_reason() {
1490        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1491<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
1492  <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">
1493    <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">
1494      <state value="form.state.in.work" signer="Test User - Manager" signerUniqueId="123456" dateSigned="2023-04-15 12:09:02 -0400" />
1495      <category name="Test Category" type="normal" highestIndex="0">
1496        <field name="test_field" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
1497          <entry id="1">
1498            <value by="Test User" byUniqueId="123456" role="Manager" when="" xml:space="preserve">Test Value</value>
1499            <reason by="Test User" byUniqueId="123456" role="Manager" when="" xml:space="preserve">Test Reason</reason>
1500          </entry>
1501        </field>
1502      </category>
1503    </form>
1504  </patient>
1505</export_from_vision_EDC>"#;
1506
1507        let result = parse_subject_native_string(xml);
1508        assert!(result.is_ok(), "Should parse successfully: {:?}", result);
1509
1510        let native = result.unwrap();
1511        assert_eq!(native.patients.len(), 1, "Should have 1 patient");
1512
1513        let patient = &native.patients[0];
1514        let forms = patient.forms.as_ref().expect("Patient should have forms");
1515        let form = &forms[0];
1516        let categories = form
1517            .categories
1518            .as_ref()
1519            .expect("Form should have categories");
1520        let fields = categories[0]
1521            .fields
1522            .as_ref()
1523            .expect("Category should have fields");
1524        let field = &fields[0];
1525        let entries = field.entries.as_ref().expect("Field should have entries");
1526        let entry = &entries[0];
1527
1528        let value = entry.value.as_ref().expect("Entry should have value");
1529        assert!(
1530            value.when.is_none(),
1531            "Empty when attribute in value should be None"
1532        );
1533        assert_eq!(value.value, "Test Value");
1534
1535        let reason = entry.reason.as_ref().expect("Entry should have reason");
1536        assert!(
1537            reason.when.is_none(),
1538            "Empty when attribute in reason should be None"
1539        );
1540        assert_eq!(reason.value, "Test Reason");
1541    }
1542}