pub mod errors;
pub mod native;
use std::{collections::HashMap, fs::read_to_string, io::Cursor, path::Path};
use rayon::prelude::*;
use crate::errors::Error;
use crate::native::{
common::{Category, Comment, Entry, Field, LockState, Reason, State, Value},
site_native::{Site, SiteNative},
subject_native::{Form, Patient, SubjectNative},
user_native::{User, UserNative},
};
use quick_xml::events::{BytesStart, Event};
use quick_xml::Reader;
pub fn parse_site_native_file(xml_path: &Path) -> Result<SiteNative, Error> {
check_valid_xml_file(xml_path)?;
let xml_file = read_to_string(xml_path)?;
let native = parse_site_native_string(&xml_file)?;
Ok(native)
}
pub fn parse_site_native_string(xml_str: &str) -> Result<SiteNative, Error> {
let chunks = extract_site_chunks(xml_str);
let sites = chunks
.into_par_iter()
.map(parse_site_xml)
.collect::<Result<Vec<_>, _>>()?;
Ok(SiteNative { sites })
}
pub fn parse_subject_native_file(xml_path: &Path) -> Result<SubjectNative, Error> {
check_valid_xml_file(xml_path)?;
let xml_str = read_to_string(xml_path)?;
let chunks = extract_patient_chunks(&xml_str);
let patients = chunks
.into_par_iter()
.map(parse_patient_xml)
.collect::<Result<Vec<_>, _>>()?;
Ok(SubjectNative { patients })
}
pub fn parse_subject_native_string(xml_str: &str) -> Result<SubjectNative, Error> {
let chunks = extract_patient_chunks(xml_str);
let patients = chunks
.into_par_iter()
.map(parse_patient_xml)
.collect::<Result<Vec<_>, _>>()?;
Ok(SubjectNative { patients })
}
fn extract_attributes(e: &BytesStart) -> Result<HashMap<String, String>, Error> {
let mut attrs = HashMap::new();
for attr in e.attributes() {
let attr = attr.map_err(|e| {
Error::ParsingError(quick_xml::de::DeError::Custom(format!(
"Attribute error: {}",
e
)))
})?;
let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
let value = String::from_utf8_lossy(&attr.value).to_string();
attrs.insert(key, value);
}
Ok(attrs)
}
fn extract_patient_chunks(xml: &str) -> Vec<&str> {
let mut chunks = Vec::new();
let mut pos = 0;
loop {
match xml[pos..].find("<patient ") {
None => break,
Some(rel) => {
let start = pos + rel;
match xml[start..].find("</patient>") {
None => break,
Some(rel_end) => {
let end = start + rel_end + "</patient>".len();
chunks.push(&xml[start..end]);
pos = end;
}
}
}
}
}
chunks
}
fn parse_patient_xml(patient_xml: &str) -> Result<Patient, Error> {
let wrapped = format!("<r>{}</r>", patient_xml);
let mut xml_reader = Reader::from_reader(Cursor::new(wrapped.as_bytes()));
xml_reader.config_mut().trim_text(true);
let mut current_patient: Option<Patient> = None;
let mut current_forms: Vec<Form> = Vec::new();
let mut current_form: Option<Form> = None;
let mut current_states: Vec<State> = Vec::new();
let mut current_categories: Vec<Category> = Vec::new();
let mut current_category: Option<Category> = None;
let mut current_fields: Vec<Field> = Vec::new();
let mut current_field: Option<Field> = None;
let mut current_entries: Vec<Entry> = Vec::new();
let mut current_entry: Option<Entry> = None;
let mut current_comments: Vec<Comment> = Vec::new();
let mut current_comment: Option<Comment> = None;
let mut current_value: Option<Value> = None;
let mut current_reason: Option<Reason> = None;
let mut text_content = String::new();
let mut in_form = false;
let mut in_category = false;
let mut in_field = false;
let mut in_entry = false;
let mut in_comment = false;
let mut in_value = false;
let mut in_reason = false;
let mut buf = Vec::new();
loop {
match xml_reader.read_event_into(&mut buf) {
Err(e) => {
return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
format!("XML reading error: {}", e),
)))
}
Ok(Event::Eof) => break,
Ok(Event::Start(ref e)) => {
let name_bytes = e.local_name();
if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
match name {
"patient" => {
let attrs = extract_attributes(e)?;
current_patient = Some(Patient::from_attributes(attrs)?);
current_forms.clear();
}
"form" if current_patient.is_some() => {
let attrs = extract_attributes(e)?;
current_form = Some(Form::from_attributes(attrs)?);
in_form = true;
current_states.clear();
current_categories.clear();
}
"category" if in_form => {
let attrs = extract_attributes(e)?;
current_category = Some(Category::from_attributes(attrs)?);
in_category = true;
current_fields.clear();
}
"field" if in_category => {
let attrs = extract_attributes(e)?;
current_field = Some(Field::from_attributes(attrs)?);
in_field = true;
current_entries.clear();
current_comments.clear();
}
"entry" if in_field => {
let attrs = extract_attributes(e)?;
current_entry = Some(Entry::from_attributes(attrs)?);
in_entry = true;
}
"comment" if in_field => {
let attrs = extract_attributes(e)?;
let comment_id = attrs.get("id").cloned().unwrap_or_default();
current_comment = Some(Comment {
comment_id,
value: None,
});
in_comment = true;
}
"value" if in_entry || in_comment => {
let attrs = extract_attributes(e)?;
current_value = Some(Value::from_attributes(attrs)?);
in_value = true;
text_content.clear();
}
"reason" if in_entry => {
let attrs = extract_attributes(e)?;
current_reason = Some(Reason::from_attributes(attrs)?);
in_reason = true;
text_content.clear();
}
_ => {}
}
}
}
Ok(Event::Text(e)) if (in_value || in_reason) => {
text_content.push_str(&String::from_utf8_lossy(&e));
}
Ok(Event::End(ref e)) => {
let name_bytes = e.local_name();
if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
match name {
"patient" => {
if let Some(mut patient) = current_patient.take() {
if !current_forms.is_empty() {
patient.set_forms(std::mem::take(&mut current_forms));
}
current_patient = Some(patient);
}
}
"form" if in_form => {
if let Some(mut form) = current_form.take() {
if !current_states.is_empty() {
form.states = Some(std::mem::take(&mut current_states));
}
if !current_categories.is_empty() {
form.categories = Some(std::mem::take(&mut current_categories));
}
current_forms.push(form);
}
in_form = false;
}
"category" if in_category => {
if let Some(mut category) = current_category.take() {
if !current_fields.is_empty() {
category.fields = Some(std::mem::take(&mut current_fields));
}
current_categories.push(category);
}
in_category = false;
}
"field" if in_field => {
if let Some(mut field) = current_field.take() {
if !current_entries.is_empty() {
field.entries = Some(std::mem::take(&mut current_entries));
}
if !current_comments.is_empty() {
field.comments = Some(std::mem::take(&mut current_comments));
}
current_fields.push(field);
}
in_field = false;
}
"entry" if in_entry => {
if let Some(entry) = current_entry.take() {
current_entries.push(entry);
}
in_entry = false;
}
"comment" if in_comment => {
if let Some(comment) = current_comment.take() {
current_comments.push(comment);
}
in_comment = false;
}
"value" if in_value => {
if let Some(mut value) = current_value.take() {
value.value = std::mem::take(&mut text_content);
if let Some(ref mut entry) = current_entry {
entry.value = Some(value);
} else if let Some(ref mut comment) = current_comment {
comment.value = Some(value);
}
}
in_value = false;
}
"reason" if in_reason => {
if let Some(mut reason) = current_reason.take() {
reason.value = std::mem::take(&mut text_content);
if let Some(ref mut entry) = current_entry {
entry.reason = Some(reason);
}
}
in_reason = false;
}
_ => {}
}
}
}
Ok(Event::Empty(ref e)) => {
let name_bytes = e.local_name();
if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
match name {
"state" if in_form => {
let attrs = extract_attributes(e)?;
let state = State::from_attributes(attrs)?;
current_states.push(state);
}
"lockState" if in_form => {
let attrs = extract_attributes(e)?;
let lock_state = LockState::from_attributes(attrs)?;
if let Some(ref mut form) = current_form {
form.lock_state = Some(lock_state);
}
}
"value" if in_entry => {
let attrs = extract_attributes(e)?;
let value = Value::from_attributes(attrs)?;
if let Some(ref mut entry) = current_entry {
entry.value = Some(value);
}
}
"reason" if in_entry => {
let attrs = extract_attributes(e)?;
let reason = Reason::from_attributes(attrs)?;
if let Some(ref mut entry) = current_entry {
entry.reason = Some(reason);
}
}
_ => {}
}
}
}
_ => {}
}
buf.clear();
}
current_patient.ok_or_else(|| {
Error::ParsingError(quick_xml::de::DeError::Custom(
"No patient found in chunk".to_string(),
))
})
}
fn extract_site_chunks(xml: &str) -> Vec<&str> {
let mut chunks = Vec::new();
let mut pos = 0;
loop {
match xml[pos..].find("<site ") {
None => break,
Some(rel) => {
let start = pos + rel;
match xml[start..].find("</site>") {
None => break,
Some(rel_end) => {
let end = start + rel_end + "</site>".len();
chunks.push(&xml[start..end]);
pos = end;
}
}
}
}
}
chunks
}
fn parse_site_xml(site_xml: &str) -> Result<Site, Error> {
let wrapped = format!("<r>{}</r>", site_xml);
let mut xml_reader = Reader::from_reader(Cursor::new(wrapped.as_bytes()));
xml_reader.config_mut().trim_text(true);
let mut current_site: Option<Site> = None;
let mut current_forms: Vec<Form> = Vec::new();
let mut current_form: Option<Form> = None;
let mut current_states: Vec<State> = Vec::new();
let mut current_categories: Vec<Category> = Vec::new();
let mut current_category: Option<Category> = None;
let mut current_fields: Vec<Field> = Vec::new();
let mut current_field: Option<Field> = None;
let mut current_entries: Vec<Entry> = Vec::new();
let mut current_entry: Option<Entry> = None;
let mut current_comments: Vec<Comment> = Vec::new();
let mut current_comment: Option<Comment> = None;
let mut current_value: Option<Value> = None;
let mut current_reason: Option<Reason> = None;
let mut text_content = String::new();
let mut in_form = false;
let mut in_category = false;
let mut in_field = false;
let mut in_entry = false;
let mut in_comment = false;
let mut in_value = false;
let mut in_reason = false;
let mut buf = Vec::new();
loop {
match xml_reader.read_event_into(&mut buf) {
Err(e) => {
return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
format!("XML reading error: {}", e),
)))
}
Ok(Event::Eof) => break,
Ok(Event::Start(ref e)) => {
let name_bytes = e.local_name();
if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
match name {
"site" => {
let attrs = extract_attributes(e)?;
current_site = Some(Site::from_attributes(attrs)?);
current_forms.clear();
}
"form" if current_site.is_some() => {
let attrs = extract_attributes(e)?;
current_form = Some(Form::from_attributes(attrs)?);
in_form = true;
current_states.clear();
current_categories.clear();
}
"category" if in_form => {
let attrs = extract_attributes(e)?;
current_category = Some(Category::from_attributes(attrs)?);
in_category = true;
current_fields.clear();
}
"field" if in_category => {
let attrs = extract_attributes(e)?;
current_field = Some(Field::from_attributes(attrs)?);
in_field = true;
current_entries.clear();
current_comments.clear();
}
"entry" if in_field => {
let attrs = extract_attributes(e)?;
current_entry = Some(Entry::from_attributes(attrs)?);
in_entry = true;
}
"comment" if in_field => {
let attrs = extract_attributes(e)?;
let comment_id = attrs.get("id").cloned().unwrap_or_default();
current_comment = Some(Comment {
comment_id,
value: None,
});
in_comment = true;
}
"value" if in_entry || in_comment => {
let attrs = extract_attributes(e)?;
current_value = Some(Value::from_attributes(attrs)?);
in_value = true;
text_content.clear();
}
"reason" if in_entry => {
let attrs = extract_attributes(e)?;
current_reason = Some(Reason::from_attributes(attrs)?);
in_reason = true;
text_content.clear();
}
_ => {}
}
}
}
Ok(Event::Text(e)) if (in_value || in_reason) => {
text_content.push_str(&String::from_utf8_lossy(&e));
}
Ok(Event::End(ref e)) => {
let name_bytes = e.local_name();
if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
match name {
"site" => {
if let Some(mut site) = current_site.take() {
if !current_forms.is_empty() {
site.set_forms(std::mem::take(&mut current_forms));
}
current_site = Some(site);
}
}
"form" if in_form => {
if let Some(mut form) = current_form.take() {
if !current_states.is_empty() {
form.states = Some(std::mem::take(&mut current_states));
}
if !current_categories.is_empty() {
form.categories = Some(std::mem::take(&mut current_categories));
}
current_forms.push(form);
}
in_form = false;
}
"category" if in_category => {
if let Some(mut category) = current_category.take() {
if !current_fields.is_empty() {
category.fields = Some(std::mem::take(&mut current_fields));
}
current_categories.push(category);
}
in_category = false;
}
"field" if in_field => {
if let Some(mut field) = current_field.take() {
if !current_entries.is_empty() {
field.entries = Some(std::mem::take(&mut current_entries));
}
if !current_comments.is_empty() {
field.comments = Some(std::mem::take(&mut current_comments));
}
current_fields.push(field);
}
in_field = false;
}
"entry" if in_entry => {
if let Some(entry) = current_entry.take() {
current_entries.push(entry);
}
in_entry = false;
}
"comment" if in_comment => {
if let Some(comment) = current_comment.take() {
current_comments.push(comment);
}
in_comment = false;
}
"value" if in_value => {
if let Some(mut value) = current_value.take() {
value.value = std::mem::take(&mut text_content);
if let Some(ref mut entry) = current_entry {
entry.value = Some(value);
} else if let Some(ref mut comment) = current_comment {
comment.value = Some(value);
}
}
in_value = false;
}
"reason" if in_reason => {
if let Some(mut reason) = current_reason.take() {
reason.value = std::mem::take(&mut text_content);
if let Some(ref mut entry) = current_entry {
entry.reason = Some(reason);
}
}
in_reason = false;
}
_ => {}
}
}
}
Ok(Event::Empty(ref e)) => {
let name_bytes = e.local_name();
if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
match name {
"state" if in_form => {
let attrs = extract_attributes(e)?;
let state = State::from_attributes(attrs)?;
current_states.push(state);
}
"lockState" if in_form => {
let attrs = extract_attributes(e)?;
let lock_state = LockState::from_attributes(attrs)?;
if let Some(ref mut form) = current_form {
form.lock_state = Some(lock_state);
}
}
"field" if in_category => {
let attrs = extract_attributes(e)?;
let field = Field::from_attributes(attrs)?;
current_fields.push(field);
}
"value" if in_entry => {
let attrs = extract_attributes(e)?;
let value = Value::from_attributes(attrs)?;
if let Some(ref mut entry) = current_entry {
entry.value = Some(value);
}
}
"reason" if in_entry => {
let attrs = extract_attributes(e)?;
let reason = Reason::from_attributes(attrs)?;
if let Some(ref mut entry) = current_entry {
entry.reason = Some(reason);
}
}
_ => {}
}
}
}
_ => {}
}
buf.clear();
}
current_site.ok_or_else(|| {
Error::ParsingError(quick_xml::de::DeError::Custom(
"No site found in chunk".to_string(),
))
})
}
pub fn parse_user_native_file(xml_path: &Path) -> Result<UserNative, Error> {
check_valid_xml_file(xml_path)?;
let xml_file = read_to_string(xml_path)?;
let native = parse_user_native_string(&xml_file)?;
Ok(native)
}
pub fn parse_user_native_string(xml_str: &str) -> Result<UserNative, Error> {
let chunks = extract_user_chunks(xml_str);
let users = chunks
.into_par_iter()
.map(parse_user_xml)
.collect::<Result<Vec<_>, _>>()?;
Ok(UserNative { users })
}
fn extract_user_chunks(xml: &str) -> Vec<&str> {
let mut chunks = Vec::new();
let mut pos = 0;
loop {
match xml[pos..].find("<user ") {
None => break,
Some(rel) => {
let start = pos + rel;
match xml[start..].find("</user>") {
None => break,
Some(rel_end) => {
let end = start + rel_end + "</user>".len();
chunks.push(&xml[start..end]);
pos = end;
}
}
}
}
}
chunks
}
fn parse_user_xml(user_xml: &str) -> Result<User, Error> {
let wrapped = format!("<r>{}</r>", user_xml);
let mut xml_reader = Reader::from_reader(Cursor::new(wrapped.as_bytes()));
xml_reader.config_mut().trim_text(true);
let mut current_user: Option<User> = None;
let mut current_forms: Vec<Form> = Vec::new();
let mut current_form: Option<Form> = None;
let mut current_states: Vec<State> = Vec::new();
let mut current_categories: Vec<Category> = Vec::new();
let mut current_category: Option<Category> = None;
let mut current_fields: Vec<Field> = Vec::new();
let mut current_field: Option<Field> = None;
let mut current_entries: Vec<Entry> = Vec::new();
let mut current_entry: Option<Entry> = None;
let mut current_comments: Vec<Comment> = Vec::new();
let mut current_comment: Option<Comment> = None;
let mut current_value: Option<Value> = None;
let mut current_reason: Option<Reason> = None;
let mut text_content = String::new();
let mut in_form = false;
let mut in_category = false;
let mut in_field = false;
let mut in_entry = false;
let mut in_comment = false;
let mut in_value = false;
let mut in_reason = false;
let mut buf = Vec::new();
loop {
match xml_reader.read_event_into(&mut buf) {
Err(e) => {
return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
format!("XML reading error: {}", e),
)))
}
Ok(Event::Eof) => break,
Ok(Event::Start(ref e)) => {
let name_bytes = e.local_name();
if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
match name {
"user" => {
let attrs = extract_attributes(e)?;
current_user = Some(User::from_attributes(attrs)?);
}
"form" => {
let attrs = extract_attributes(e)?;
current_form = Some(Form::from_attributes(attrs)?);
in_form = true;
}
"category" if in_form => {
let attrs = extract_attributes(e)?;
current_category = Some(Category::from_attributes(attrs)?);
in_category = true;
}
"field" if in_category => {
let attrs = extract_attributes(e)?;
current_field = Some(Field::from_attributes(attrs)?);
in_field = true;
}
"entry" if in_field => {
let attrs = extract_attributes(e)?;
current_entry = Some(Entry::from_attributes(attrs)?);
in_entry = true;
}
"comment" if in_field => {
let attrs = extract_attributes(e)?;
let comment_id = attrs.get("id").cloned().unwrap_or_default();
current_comment = Some(Comment {
comment_id,
value: None,
});
in_comment = true;
}
"value" if in_entry || in_comment => {
let attrs = extract_attributes(e)?;
current_value = Some(Value::from_attributes(attrs)?);
in_value = true;
text_content.clear();
}
"reason" if in_entry => {
let attrs = extract_attributes(e)?;
current_reason = Some(Reason::from_attributes(attrs)?);
in_reason = true;
text_content.clear();
}
_ => {}
}
}
}
Ok(Event::Text(e)) if (in_value || in_reason) => {
text_content.push_str(&String::from_utf8_lossy(&e));
}
Ok(Event::End(ref e)) => {
let name_bytes = e.local_name();
if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
match name {
"user" => {
if let Some(mut user) = current_user.take() {
if !current_forms.is_empty() {
user.set_forms(std::mem::take(&mut current_forms));
}
current_user = Some(user);
}
}
"form" if in_form => {
if let Some(mut form) = current_form.take() {
if !current_states.is_empty() {
form.states = Some(std::mem::take(&mut current_states));
}
if !current_categories.is_empty() {
form.categories = Some(std::mem::take(&mut current_categories));
}
current_forms.push(form);
}
in_form = false;
}
"category" if in_category => {
if let Some(mut category) = current_category.take() {
if !current_fields.is_empty() {
category.fields = Some(std::mem::take(&mut current_fields));
}
current_categories.push(category);
}
in_category = false;
}
"field" if in_field => {
if let Some(mut field) = current_field.take() {
if !current_entries.is_empty() {
field.entries = Some(std::mem::take(&mut current_entries));
}
if !current_comments.is_empty() {
field.comments = Some(std::mem::take(&mut current_comments));
}
current_fields.push(field);
}
in_field = false;
}
"entry" if in_entry => {
if let Some(entry) = current_entry.take() {
current_entries.push(entry);
}
in_entry = false;
}
"comment" if in_comment => {
if let Some(comment) = current_comment.take() {
current_comments.push(comment);
}
in_comment = false;
}
"value" if in_value => {
if let Some(mut value) = current_value.take() {
value.value = std::mem::take(&mut text_content);
if let Some(ref mut entry) = current_entry {
entry.value = Some(value);
} else if let Some(ref mut comment) = current_comment {
comment.value = Some(value);
}
}
in_value = false;
}
"reason" if in_reason => {
if let Some(mut reason) = current_reason.take() {
reason.value = std::mem::take(&mut text_content);
if let Some(ref mut entry) = current_entry {
entry.reason = Some(reason);
}
}
in_reason = false;
}
_ => {}
}
}
}
Ok(Event::Empty(ref e)) => {
let name_bytes = e.local_name();
if let Ok(name) = std::str::from_utf8(name_bytes.as_ref()) {
match name {
"state" if in_form => {
let attrs = extract_attributes(e)?;
let state = State::from_attributes(attrs)?;
current_states.push(state);
}
"field" if in_category => {
let attrs = extract_attributes(e)?;
let field = Field::from_attributes(attrs)?;
current_fields.push(field);
}
_ => {}
}
}
}
_ => {}
}
buf.clear();
}
current_user.ok_or_else(|| {
Error::ParsingError(quick_xml::de::DeError::Custom(
"No user element found".to_string(),
))
})
}
fn check_valid_xml_file(xml_path: &Path) -> Result<(), Error> {
if !xml_path.exists() {
return Err(Error::FileNotFound(xml_path.to_path_buf()));
}
if let Some(extension) = xml_path.extension() {
if extension != "xml" {
return Err(Error::InvalidFileType(xml_path.to_owned()));
}
} else {
return Err(Error::Unknown);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::{tempdir, Builder};
#[test]
fn test_site_file_not_found_error() {
let dir = tempdir().unwrap().path().to_path_buf();
let result = parse_site_native_file(&dir);
assert!(result.is_err());
assert!(matches!(result, Err(Error::FileNotFound(_))));
}
#[test]
fn test_site_invalid_file_type_error() {
let file = Builder::new()
.prefix("test")
.suffix(".csv")
.tempfile()
.unwrap();
let result = parse_site_native_file(file.path());
assert!(result.is_err());
assert!(matches!(result, Err(Error::InvalidFileType(_))));
}
#[test]
fn test_subject_file_not_found_error() {
let dir = tempdir().unwrap().path().to_path_buf();
let result = parse_subject_native_file(&dir);
assert!(result.is_err());
assert!(matches!(result, Err(Error::FileNotFound(_))));
}
#[test]
fn test_subject_invalid_file_type_error() {
let file = Builder::new()
.prefix("test")
.suffix(".csv")
.tempfile()
.unwrap();
let result = parse_subject_native_file(file.path());
assert!(result.is_err());
assert!(matches!(result, Err(Error::InvalidFileType(_))));
}
#[test]
fn test_user_file_not_found_error() {
let dir = tempdir().unwrap().path().to_path_buf();
let result = parse_user_native_file(&dir);
assert!(result.is_err());
assert!(matches!(result, Err(Error::FileNotFound(_))));
}
#[test]
fn test_user_invalid_file_type_error() {
let file = Builder::new()
.prefix("test")
.suffix(".csv")
.tempfile()
.unwrap();
let result = parse_user_native_file(file.path());
assert!(result.is_err());
assert!(matches!(result, Err(Error::InvalidFileType(_))));
}
#[test]
fn test_forms_parsing_regression() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
<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">
<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">
<state value="form.state.in.work" signer="Test User - Tester" signerUniqueId="111111111" dateSigned="2023-04-15 12:09:02 -0400"/>
<category name="Test Category" type="normal" highestIndex="0">
<field name="test_field" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
<entry id="1">
<value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Test Value</value>
</entry>
</field>
</category>
</form>
<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">
<state value="form.state.complete" signer="Test User - Tester" signerUniqueId="111111111" dateSigned="2023-04-15 12:10:02 -0400"/>
</form>
</patient>
</export_from_vision_EDC>"#;
let result = parse_subject_native_string(xml).expect("Should parse successfully");
assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
let patient = &result.patients[0];
assert_eq!(patient.patient_id, "TEST-001");
assert_eq!(patient.number_of_forms, 2);
let forms = patient.forms.as_ref().expect("Patient should have forms");
assert_eq!(forms.len(), 2, "Patient should have exactly 2 forms");
let form1 = &forms[0];
assert_eq!(form1.name, "test.form.1");
assert_eq!(form1.form_title, "Test Form 1");
assert_eq!(form1.form_index, 1);
assert_eq!(form1.form_state, "In-Work");
let states1 = form1.states.as_ref().expect("Form 1 should have states");
assert_eq!(states1.len(), 1);
assert_eq!(states1[0].value, "form.state.in.work");
let categories1 = form1
.categories
.as_ref()
.expect("Form 1 should have categories");
assert_eq!(categories1.len(), 1);
assert_eq!(categories1[0].name, "Test Category");
let fields1 = categories1[0]
.fields
.as_ref()
.expect("Category should have fields");
assert_eq!(fields1.len(), 1);
assert_eq!(fields1[0].name, "test_field");
let entries1 = fields1[0]
.entries
.as_ref()
.expect("Field should have entries");
assert_eq!(entries1.len(), 1);
assert_eq!(entries1[0].entry_id, "1");
let value1 = entries1[0].value.as_ref().expect("Entry should have value");
assert_eq!(value1.value, "Test Value");
assert_eq!(value1.by, "Test User");
assert_eq!(value1.role, "Tester");
let form2 = &forms[1];
assert_eq!(form2.name, "test.form.2");
assert_eq!(form2.form_title, "Test Form 2");
assert_eq!(form2.form_index, 2);
assert_eq!(form2.form_state, "Complete");
let states2 = form2.states.as_ref().expect("Form 2 should have states");
assert_eq!(states2.len(), 1);
assert_eq!(states2[0].value, "form.state.complete");
}
#[test]
fn test_comments_parsing_regression() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
<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">
<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">
<category name="Test Category" type="normal" highestIndex="0">
<field name="field_with_comments" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
<entry id="1">
<value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Field Value</value>
</entry>
<comment id="1">
<value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:05 -0400" xml:space="preserve">First comment</value>
</comment>
<comment id="2">
<value by="Another User" byUniqueId="222222222" role="Reviewer" when="2023-04-15 12:10:00 -0400" xml:space="preserve">Second comment</value>
</comment>
</field>
<field name="field_without_comments" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:30 -0400" keepHistory="true">
<entry id="1">
<value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:10 -0400" xml:space="preserve">Another Value</value>
</entry>
</field>
</category>
</form>
</patient>
</export_from_vision_EDC>"#;
let result = parse_subject_native_string(xml).expect("Should parse successfully");
assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
let patient = &result.patients[0];
let forms = patient.forms.as_ref().expect("Patient should have forms");
let form = &forms[0];
let categories = form
.categories
.as_ref()
.expect("Form should have categories");
let fields = categories[0]
.fields
.as_ref()
.expect("Category should have fields");
assert_eq!(fields.len(), 2, "Should have 2 fields");
let field_with_comments = &fields[0];
assert_eq!(field_with_comments.name, "field_with_comments");
let comments = field_with_comments
.comments
.as_ref()
.expect("Field should have comments");
assert_eq!(comments.len(), 2, "Should have exactly 2 comments");
let comment1 = &comments[0];
assert_eq!(comment1.comment_id, "1");
let comment1_value = comment1
.value
.as_ref()
.expect("Comment 1 should have value");
assert_eq!(comment1_value.value, "First comment");
assert_eq!(comment1_value.by, "Test User");
assert_eq!(comment1_value.role, "Tester");
let comment2 = &comments[1];
assert_eq!(comment2.comment_id, "2");
let comment2_value = comment2
.value
.as_ref()
.expect("Comment 2 should have value");
assert_eq!(comment2_value.value, "Second comment");
assert_eq!(comment2_value.by, "Another User");
assert_eq!(comment2_value.role, "Reviewer");
let field_without_comments = &fields[1];
assert_eq!(field_without_comments.name, "field_without_comments");
assert!(
field_without_comments.comments.is_none(),
"Field without comments should have no comments"
);
}
#[test]
fn test_empty_forms_handling() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
<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">
</patient>
</export_from_vision_EDC>"#;
let result = parse_subject_native_string(xml).expect("Should parse successfully");
assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
let patient = &result.patients[0];
assert_eq!(patient.patient_id, "TEST-003");
assert_eq!(patient.number_of_forms, 0);
assert!(
patient.forms.is_none(),
"Patient with 0 forms should have None for forms"
);
}
#[test]
fn test_large_patient_forms_regression() {
let mut xml = String::from(
r#"<?xml version="1.0" encoding="UTF-8"?>
<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
<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">"#,
);
for i in 1..=50 {
xml.push_str(&format!(r#"
<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">
<state value="form.state.in.work" signer="Test User - Tester" signerUniqueId="111111111" dateSigned="2023-04-15 12:09:02 -0400"/>
<category name="Category {}" type="normal" highestIndex="0">
<field name="field_{}" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
<entry id="1">
<value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Value {}</value>
</entry>
<comment id="1">
<value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:05 -0400" xml:space="preserve">Comment for form {}</value>
</comment>
</field>
</category>
</form>"#, i, i, i, i, i, i, i, i));
}
xml.push_str(
r#"
</patient>
</export_from_vision_EDC>"#,
);
let result =
parse_subject_native_string(&xml).expect("Should parse large patient successfully");
assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
let patient = &result.patients[0];
assert_eq!(patient.patient_id, "LARGE-TEST");
assert_eq!(patient.number_of_forms, 50);
let forms = patient.forms.as_ref().expect("Patient should have forms");
assert_eq!(forms.len(), 50, "Patient should have exactly 50 forms");
for (i, form) in forms.iter().enumerate() {
let form_num = i + 1;
assert_eq!(form.name, format!("test.form.{}", form_num));
assert_eq!(form.form_title, format!("Test Form {}", form_num));
assert_eq!(form.form_index, form_num);
let categories = form
.categories
.as_ref()
.expect("Form should have categories");
assert_eq!(categories.len(), 1);
let fields = categories[0]
.fields
.as_ref()
.expect("Category should have fields");
assert_eq!(fields.len(), 1);
let entries = fields[0]
.entries
.as_ref()
.expect("Field should have entries");
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].value.as_ref().unwrap().value,
format!("Value {}", form_num)
);
let comments = fields[0]
.comments
.as_ref()
.expect("Field should have comments");
assert_eq!(comments.len(), 1);
assert_eq!(
comments[0].value.as_ref().unwrap().value,
format!("Comment for form {}", form_num)
);
}
}
#[test]
fn test_malformed_datetime_handling() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
<patient patientId="TEST-004" uniqueId="123456793" whenCreated="" creator="Test User" siteName="Test Site" siteUniqueId="987654321" lastLanguage="English" numberOfForms="1">
<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">
<category name="Test Category" type="normal" highestIndex="0">
<field name="test_field" type="text" dataType="string" errorCode="valid" whenCreated="" keepHistory="true">
<entry id="1">
<value by="Test User" byUniqueId="111111111" role="Tester" when="2023-04-15 12:09:02 -0400" xml:space="preserve">Test Value</value>
</entry>
</field>
</category>
</form>
</patient>
</export_from_vision_EDC>"#;
let result =
parse_subject_native_string(xml).expect("Should handle malformed datetimes gracefully");
assert_eq!(result.patients.len(), 1, "Should have exactly 1 patient");
let patient = &result.patients[0];
assert!(
patient.when_created.is_none(),
"Empty whenCreated should be None"
);
let forms = patient.forms.as_ref().expect("Patient should have forms");
let form = &forms[0];
assert!(
form.last_modified.is_none(),
"Empty lastModified should be None"
);
let categories = form
.categories
.as_ref()
.expect("Form should have categories");
let fields = categories[0]
.fields
.as_ref()
.expect("Category should have fields");
let field = &fields[0];
assert!(
field.when_created.is_none(),
"Empty whenCreated in field should be None"
);
}
#[test]
fn test_empty_datetime_in_value_and_reason() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<export_from_vision_EDC date="30-May-2024 10:35 -0500" createdBy="Test User" role="Project Manager" numberSubjectsProcessed="1">
<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">
<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">
<state value="form.state.in.work" signer="Test User - Manager" signerUniqueId="123456" dateSigned="2023-04-15 12:09:02 -0400" />
<category name="Test Category" type="normal" highestIndex="0">
<field name="test_field" type="text" dataType="string" errorCode="valid" whenCreated="2023-04-15 12:08:26 -0400" keepHistory="true">
<entry id="1">
<value by="Test User" byUniqueId="123456" role="Manager" when="" xml:space="preserve">Test Value</value>
<reason by="Test User" byUniqueId="123456" role="Manager" when="" xml:space="preserve">Test Reason</reason>
</entry>
</field>
</category>
</form>
</patient>
</export_from_vision_EDC>"#;
let result = parse_subject_native_string(xml);
assert!(result.is_ok(), "Should parse successfully: {:?}", result);
let native = result.unwrap();
assert_eq!(native.patients.len(), 1, "Should have 1 patient");
let patient = &native.patients[0];
let forms = patient.forms.as_ref().expect("Patient should have forms");
let form = &forms[0];
let categories = form
.categories
.as_ref()
.expect("Form should have categories");
let fields = categories[0]
.fields
.as_ref()
.expect("Category should have fields");
let field = &fields[0];
let entries = field.entries.as_ref().expect("Field should have entries");
let entry = &entries[0];
let value = entry.value.as_ref().expect("Entry should have value");
assert!(
value.when.is_none(),
"Empty when attribute in value should be None"
);
assert_eq!(value.value, "Test Value");
let reason = entry.reason.as_ref().expect("Entry should have reason");
assert!(
reason.when.is_none(),
"Empty when attribute in reason should be None"
);
assert_eq!(reason.value, "Test Reason");
}
}