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