pub mod errors;
pub mod native;
use std::{fs::read_to_string, path::Path, sync::Arc};
use rayon::prelude::*;
use crate::{
errors::Error,
native::{
common::{
Category, Comment, Entry, Export, Field, File, LockState, Query, Reason, State, Value,
},
deserializers::{decode_error, push_general_ref, take_trimmed, Interner},
site_native::{Site, SiteNative},
subject_native::{Form, Patient, SubjectNative},
user_native::{User, UserNative},
},
};
use quick_xml::{events::Event, 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 {
export: parse_export(xml_str)?,
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 {
export: parse_export(&xml_str)?,
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 {
export: parse_export(xml_str)?,
patients,
})
}
fn parse_export(xml: &str) -> Result<Option<Export>, Error> {
if std::env::var("SKIP_EXPORT").is_ok() {
return Ok(None);
}
let mut reader = Reader::from_str(xml);
loop {
match reader.read_event() {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
return Export::from_attributes(e).map(Some)
}
Ok(Event::Eof) => return Ok(None),
Err(e) => {
return Err(Error::ParsingError(quick_xml::de::DeError::Custom(
format!("XML reading error: {}", e),
)))
}
_ => {}
}
}
}
fn extract_chunks<'a>(xml: &'a str, tag: &str) -> Vec<&'a str> {
let open = format!("<{tag}");
let close = format!("</{tag}>");
let mut chunks = Vec::new();
let mut pos = 0;
while let Some(rel) = xml[pos..].find(&open) {
let start = pos + rel;
let after_name = start + open.len();
let ends_name = xml[after_name..]
.chars()
.next()
.is_some_and(|c| c.is_whitespace() || c == '>');
if !ends_name {
pos = after_name;
continue;
}
let Some(rel_end) = xml[start..].find(&close) else {
break;
};
let end = start + rel_end + close.len();
chunks.push(&xml[start..end]);
pos = end;
}
chunks
}
fn extract_patient_chunks(xml: &str) -> Vec<&str> {
extract_chunks(xml, "patient")
}
#[allow(clippy::drain_collect)]
fn parse_patient_xml(patient_xml: &str) -> Result<Patient, Error> {
let mut interner = Interner::default();
let mut xml_reader = Reader::from_str(patient_xml);
xml_reader.config_mut().trim_text(false);
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_lock_states: Vec<LockState> = 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_files: Vec<File> = Vec::new();
let mut current_file: Option<File> = None;
let mut current_queries: Vec<Query> = Vec::new();
let mut current_query: Option<Query> = None;
let mut current_download_history: Vec<Comment> = Vec::new();
let mut current_download_entry: 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_file = false;
let mut in_query = false;
let mut in_download_history = false;
let mut in_value = false;
let mut in_reason = false;
loop {
match xml_reader.read_event() {
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" => {
current_patient = Some(Patient::from_attributes(e)?);
current_forms.clear();
}
"form" if current_patient.is_some() => {
current_form = Some(Form::from_attributes(e)?);
in_form = true;
current_states.clear();
current_lock_states.clear();
current_categories.clear();
}
"category" if in_form => {
current_category = Some(Category::from_attributes(e, &mut interner)?);
in_category = true;
current_fields.clear();
}
"field" if in_category => {
current_field = Some(Field::from_attributes(e, &mut interner)?);
in_field = true;
current_entries.clear();
current_comments.clear();
current_queries.clear();
}
"file" if in_category => {
current_file = Some(File::from_attributes(e, &mut interner)?);
in_file = true;
current_entries.clear();
current_comments.clear();
current_queries.clear();
current_download_history.clear();
}
"entry" if in_field || in_file => {
current_entry = Some(Entry::from_attributes(e, &mut interner)?);
in_entry = true;
}
"comment" if in_field || in_file => {
current_comment = Some(Comment::from_attributes(e)?);
in_comment = true;
}
"query" if in_field || in_file => {
current_query = Some(Query::from_attributes(e, &mut interner)?);
in_query = true;
}
"downloadHistory" if in_file => {
current_download_entry = Some(Comment::from_attributes(e)?);
in_download_history = true;
}
"answer" if in_query => {
current_value = Some(Value::from_attributes(e, &mut interner)?);
in_value = true;
text_content.clear();
}
"value" if in_entry || in_comment || in_query || in_download_history => {
current_value = Some(Value::from_attributes(e, &mut interner)?);
in_value = true;
text_content.clear();
}
"reason" if in_entry => {
current_reason = Some(Reason::from_attributes(e, &mut interner)?);
in_reason = true;
text_content.clear();
}
_ => {}
}
}
}
Ok(Event::Text(e)) if (in_value || in_reason) => {
text_content.push_str(&e.xml10_content().map_err(decode_error)?);
}
Ok(Event::GeneralRef(ref e)) if (in_value || in_reason) => {
push_general_ref(&mut text_content, 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(current_forms.drain(..).collect());
}
current_patient = Some(patient);
}
}
"form" if in_form => {
if let Some(mut form) = current_form.take() {
if !current_states.is_empty() {
form.states =
Some(Arc::new(current_states.drain(..).collect()));
}
if !current_lock_states.is_empty() {
form.lock_states =
Some(Arc::new(current_lock_states.drain(..).collect()));
}
if !current_categories.is_empty() {
form.categories =
Some(Arc::new(current_categories.drain(..).collect()));
}
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(Arc::new(current_fields.drain(..).collect()));
}
if !current_files.is_empty() {
category.files =
Some(Arc::new(current_files.drain(..).collect()));
}
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(Arc::new(current_entries.drain(..).collect()));
}
if !current_comments.is_empty() {
field.comments =
Some(Arc::new(current_comments.drain(..).collect()));
}
if !current_queries.is_empty() {
field.queries =
Some(Arc::new(current_queries.drain(..).collect()));
}
current_fields.push(field);
}
in_field = false;
}
"file" if in_file => {
if let Some(mut file) = current_file.take() {
if !current_entries.is_empty() {
file.entries =
Some(Arc::new(current_entries.drain(..).collect()));
}
if !current_comments.is_empty() {
file.comments =
Some(Arc::new(current_comments.drain(..).collect()));
}
if !current_queries.is_empty() {
file.queries =
Some(Arc::new(current_queries.drain(..).collect()));
}
if !current_download_history.is_empty() {
file.download_history = Some(Arc::new(
current_download_history.drain(..).collect(),
));
}
current_files.push(file);
}
in_file = false;
}
"query" if in_query => {
if let Some(query) = current_query.take() {
current_queries.push(query);
}
in_query = false;
}
"downloadHistory" if in_download_history => {
if let Some(download) = current_download_entry.take() {
current_download_history.push(download);
}
in_download_history = 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;
}
"answer" if in_value => {
if let Some(mut value) = current_value.take() {
value.value = take_trimmed(&mut text_content);
if let Some(ref mut query) = current_query {
query.answer = Some(value);
}
}
in_value = false;
}
"value" if in_value => {
if let Some(mut value) = current_value.take() {
value.value = take_trimmed(&mut text_content);
if let Some(ref mut download) = current_download_entry {
download.value = Some(value);
} else if let Some(ref mut query) = current_query {
query.value = Some(value);
} else 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 = take_trimmed(&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 state = State::from_attributes(e, &mut interner)?;
current_states.push(state);
}
"lockState" if in_form => {
current_lock_states.push(LockState::from_attributes(e)?);
}
"category" if in_form => {
current_categories.push(Category::from_attributes(e, &mut interner)?);
}
"field" if in_category => {
current_fields.push(Field::from_attributes(e, &mut interner)?);
}
"file" if in_category => {
current_files.push(File::from_attributes(e, &mut interner)?);
}
"value" if in_entry => {
let value = Value::from_attributes(e, &mut interner)?;
if let Some(ref mut entry) = current_entry {
entry.value = Some(value);
}
}
"reason" if in_entry => {
let reason = Reason::from_attributes(e, &mut interner)?;
if let Some(ref mut entry) = current_entry {
entry.reason = Some(reason);
}
}
_ => {}
}
}
}
_ => {}
}
}
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> {
extract_chunks(xml, "site")
}
#[allow(clippy::drain_collect)]
fn parse_site_xml(site_xml: &str) -> Result<Site, Error> {
let mut interner = Interner::default();
let mut xml_reader = Reader::from_str(site_xml);
xml_reader.config_mut().trim_text(false);
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_lock_states: Vec<LockState> = 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_files: Vec<File> = Vec::new();
let mut current_file: Option<File> = None;
let mut current_queries: Vec<Query> = Vec::new();
let mut current_query: Option<Query> = None;
let mut current_download_history: Vec<Comment> = Vec::new();
let mut current_download_entry: 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_file = false;
let mut in_query = false;
let mut in_download_history = false;
let mut in_value = false;
let mut in_reason = false;
loop {
match xml_reader.read_event() {
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" => {
current_site = Some(Site::from_attributes(e)?);
current_forms.clear();
}
"form" if current_site.is_some() => {
current_form = Some(Form::from_attributes(e)?);
in_form = true;
current_states.clear();
current_lock_states.clear();
current_categories.clear();
}
"category" if in_form => {
current_category = Some(Category::from_attributes(e, &mut interner)?);
in_category = true;
current_fields.clear();
}
"field" if in_category => {
current_field = Some(Field::from_attributes(e, &mut interner)?);
in_field = true;
current_entries.clear();
current_comments.clear();
current_queries.clear();
}
"file" if in_category => {
current_file = Some(File::from_attributes(e, &mut interner)?);
in_file = true;
current_entries.clear();
current_comments.clear();
current_queries.clear();
current_download_history.clear();
}
"entry" if in_field || in_file => {
current_entry = Some(Entry::from_attributes(e, &mut interner)?);
in_entry = true;
}
"comment" if in_field || in_file => {
current_comment = Some(Comment::from_attributes(e)?);
in_comment = true;
}
"query" if in_field || in_file => {
current_query = Some(Query::from_attributes(e, &mut interner)?);
in_query = true;
}
"downloadHistory" if in_file => {
current_download_entry = Some(Comment::from_attributes(e)?);
in_download_history = true;
}
"answer" if in_query => {
current_value = Some(Value::from_attributes(e, &mut interner)?);
in_value = true;
text_content.clear();
}
"value" if in_entry || in_comment || in_query || in_download_history => {
current_value = Some(Value::from_attributes(e, &mut interner)?);
in_value = true;
text_content.clear();
}
"reason" if in_entry => {
current_reason = Some(Reason::from_attributes(e, &mut interner)?);
in_reason = true;
text_content.clear();
}
_ => {}
}
}
}
Ok(Event::Text(e)) if (in_value || in_reason) => {
text_content.push_str(&e.xml10_content().map_err(decode_error)?);
}
Ok(Event::GeneralRef(ref e)) if (in_value || in_reason) => {
push_general_ref(&mut text_content, 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(current_forms.drain(..).collect());
}
current_site = Some(site);
}
}
"form" if in_form => {
if let Some(mut form) = current_form.take() {
if !current_states.is_empty() {
form.states =
Some(Arc::new(current_states.drain(..).collect()));
}
if !current_lock_states.is_empty() {
form.lock_states =
Some(Arc::new(current_lock_states.drain(..).collect()));
}
if !current_categories.is_empty() {
form.categories =
Some(Arc::new(current_categories.drain(..).collect()));
}
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(Arc::new(current_fields.drain(..).collect()));
}
if !current_files.is_empty() {
category.files =
Some(Arc::new(current_files.drain(..).collect()));
}
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(Arc::new(current_entries.drain(..).collect()));
}
if !current_comments.is_empty() {
field.comments =
Some(Arc::new(current_comments.drain(..).collect()));
}
if !current_queries.is_empty() {
field.queries =
Some(Arc::new(current_queries.drain(..).collect()));
}
current_fields.push(field);
}
in_field = false;
}
"file" if in_file => {
if let Some(mut file) = current_file.take() {
if !current_entries.is_empty() {
file.entries =
Some(Arc::new(current_entries.drain(..).collect()));
}
if !current_comments.is_empty() {
file.comments =
Some(Arc::new(current_comments.drain(..).collect()));
}
if !current_queries.is_empty() {
file.queries =
Some(Arc::new(current_queries.drain(..).collect()));
}
if !current_download_history.is_empty() {
file.download_history = Some(Arc::new(
current_download_history.drain(..).collect(),
));
}
current_files.push(file);
}
in_file = false;
}
"query" if in_query => {
if let Some(query) = current_query.take() {
current_queries.push(query);
}
in_query = false;
}
"downloadHistory" if in_download_history => {
if let Some(download) = current_download_entry.take() {
current_download_history.push(download);
}
in_download_history = 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;
}
"answer" if in_value => {
if let Some(mut value) = current_value.take() {
value.value = take_trimmed(&mut text_content);
if let Some(ref mut query) = current_query {
query.answer = Some(value);
}
}
in_value = false;
}
"value" if in_value => {
if let Some(mut value) = current_value.take() {
value.value = take_trimmed(&mut text_content);
if let Some(ref mut download) = current_download_entry {
download.value = Some(value);
} else if let Some(ref mut query) = current_query {
query.value = Some(value);
} else 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 = take_trimmed(&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 state = State::from_attributes(e, &mut interner)?;
current_states.push(state);
}
"lockState" if in_form => {
current_lock_states.push(LockState::from_attributes(e)?);
}
"category" if in_form => {
current_categories.push(Category::from_attributes(e, &mut interner)?);
}
"field" if in_category => {
let field = Field::from_attributes(e, &mut interner)?;
current_fields.push(field);
}
"file" if in_category => {
current_files.push(File::from_attributes(e, &mut interner)?);
}
"value" if in_entry => {
let value = Value::from_attributes(e, &mut interner)?;
if let Some(ref mut entry) = current_entry {
entry.value = Some(value);
}
}
"reason" if in_entry => {
let reason = Reason::from_attributes(e, &mut interner)?;
if let Some(ref mut entry) = current_entry {
entry.reason = Some(reason);
}
}
_ => {}
}
}
}
_ => {}
}
}
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 {
export: parse_export(xml_str)?,
users,
})
}
fn extract_user_chunks(xml: &str) -> Vec<&str> {
extract_chunks(xml, "user")
}
#[allow(clippy::drain_collect)]
fn parse_user_xml(user_xml: &str) -> Result<User, Error> {
let mut interner = Interner::default();
let mut xml_reader = Reader::from_str(user_xml);
xml_reader.config_mut().trim_text(false);
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_lock_states: Vec<LockState> = 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_files: Vec<File> = Vec::new();
let mut current_file: Option<File> = None;
let mut current_queries: Vec<Query> = Vec::new();
let mut current_query: Option<Query> = None;
let mut current_download_history: Vec<Comment> = Vec::new();
let mut current_download_entry: 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_file = false;
let mut in_query = false;
let mut in_download_history = false;
let mut in_value = false;
let mut in_reason = false;
loop {
match xml_reader.read_event() {
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" => {
current_user = Some(User::from_attributes(e)?);
}
"form" => {
current_form = Some(Form::from_attributes(e)?);
in_form = true;
}
"category" if in_form => {
current_category = Some(Category::from_attributes(e, &mut interner)?);
in_category = true;
}
"field" if in_category => {
current_field = Some(Field::from_attributes(e, &mut interner)?);
in_field = true;
}
"file" if in_category => {
current_file = Some(File::from_attributes(e, &mut interner)?);
in_file = true;
}
"entry" if in_field || in_file => {
current_entry = Some(Entry::from_attributes(e, &mut interner)?);
in_entry = true;
}
"comment" if in_field || in_file => {
current_comment = Some(Comment::from_attributes(e)?);
in_comment = true;
}
"query" if in_field || in_file => {
current_query = Some(Query::from_attributes(e, &mut interner)?);
in_query = true;
}
"downloadHistory" if in_file => {
current_download_entry = Some(Comment::from_attributes(e)?);
in_download_history = true;
}
"answer" if in_query => {
current_value = Some(Value::from_attributes(e, &mut interner)?);
in_value = true;
text_content.clear();
}
"value" if in_entry || in_comment || in_query || in_download_history => {
current_value = Some(Value::from_attributes(e, &mut interner)?);
in_value = true;
text_content.clear();
}
"reason" if in_entry => {
current_reason = Some(Reason::from_attributes(e, &mut interner)?);
in_reason = true;
text_content.clear();
}
_ => {}
}
}
}
Ok(Event::Text(e)) if (in_value || in_reason) => {
text_content.push_str(&e.xml10_content().map_err(decode_error)?);
}
Ok(Event::GeneralRef(ref e)) if (in_value || in_reason) => {
push_general_ref(&mut text_content, 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(current_forms.drain(..).collect());
}
current_user = Some(user);
}
}
"form" if in_form => {
if let Some(mut form) = current_form.take() {
if !current_states.is_empty() {
form.states =
Some(Arc::new(current_states.drain(..).collect()));
}
if !current_lock_states.is_empty() {
form.lock_states =
Some(Arc::new(current_lock_states.drain(..).collect()));
}
if !current_categories.is_empty() {
form.categories =
Some(Arc::new(current_categories.drain(..).collect()));
}
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(Arc::new(current_fields.drain(..).collect()));
}
if !current_files.is_empty() {
category.files =
Some(Arc::new(current_files.drain(..).collect()));
}
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(Arc::new(current_entries.drain(..).collect()));
}
if !current_comments.is_empty() {
field.comments =
Some(Arc::new(current_comments.drain(..).collect()));
}
if !current_queries.is_empty() {
field.queries =
Some(Arc::new(current_queries.drain(..).collect()));
}
current_fields.push(field);
}
in_field = false;
}
"file" if in_file => {
if let Some(mut file) = current_file.take() {
if !current_entries.is_empty() {
file.entries =
Some(Arc::new(current_entries.drain(..).collect()));
}
if !current_comments.is_empty() {
file.comments =
Some(Arc::new(current_comments.drain(..).collect()));
}
if !current_queries.is_empty() {
file.queries =
Some(Arc::new(current_queries.drain(..).collect()));
}
if !current_download_history.is_empty() {
file.download_history = Some(Arc::new(
current_download_history.drain(..).collect(),
));
}
current_files.push(file);
}
in_file = false;
}
"query" if in_query => {
if let Some(query) = current_query.take() {
current_queries.push(query);
}
in_query = false;
}
"downloadHistory" if in_download_history => {
if let Some(download) = current_download_entry.take() {
current_download_history.push(download);
}
in_download_history = 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;
}
"answer" if in_value => {
if let Some(mut value) = current_value.take() {
value.value = take_trimmed(&mut text_content);
if let Some(ref mut query) = current_query {
query.answer = Some(value);
}
}
in_value = false;
}
"value" if in_value => {
if let Some(mut value) = current_value.take() {
value.value = take_trimmed(&mut text_content);
if let Some(ref mut download) = current_download_entry {
download.value = Some(value);
} else if let Some(ref mut query) = current_query {
query.value = Some(value);
} else 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 = take_trimmed(&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 state = State::from_attributes(e, &mut interner)?;
current_states.push(state);
}
"category" if in_form => {
current_categories.push(Category::from_attributes(e, &mut interner)?);
}
"field" if in_category => {
let field = Field::from_attributes(e, &mut interner)?;
current_fields.push(field);
}
"file" if in_category => {
current_files.push(File::from_attributes(e, &mut interner)?);
}
"lockState" if in_form => {
current_lock_states.push(LockState::from_attributes(e)?);
}
"value" if in_entry => {
let value = Value::from_attributes(e, &mut interner)?;
if let Some(ref mut entry) = current_entry {
entry.value = Some(value);
}
}
"reason" if in_entry => {
let reason = Reason::from_attributes(e, &mut interner)?;
if let Some(ref mut entry) = current_entry {
entry.reason = Some(reason);
}
}
_ => {}
}
}
}
_ => {}
}
}
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.is_file() {
return Err(Error::FileNotFound(xml_path.to_path_buf()));
}
if let Some(extension) = xml_path.extension() {
if !extension.eq_ignore_ascii_case("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");
}
}