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