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