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