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