use std::sync::Arc;
use chrono::{DateTime, Utc};
#[cfg(feature = "python")]
use pyo3::{
exceptions::PyValueError,
prelude::*,
types::{PyDateTime, PyDict},
};
use quick_xml::events::BytesStart;
use crate::native::deserializers::{
checked_datetime, deserialize_empty_string_as_none_datetime, optional_string,
required_attribute, visit_attributes,
};
#[cfg(feature = "python")]
use crate::native::deserializers::{deserialize_empty_string_as_none, to_py_datetime};
use serde::{Deserialize, Serialize};
pub use crate::native::common::{
Category, Comment, Entry, Export, Field, Form, Reason, State, Value,
};
#[cfg(not(feature = "python"))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Patient {
#[serde(rename = "patientId")]
pub patient_id: String,
#[serde(rename = "uniqueId")]
pub unique_id: String,
#[serde(rename = "whenCreated")]
pub when_created: Option<DateTime<Utc>>,
#[serde(rename = "passwordChangeDate")]
#[serde(
default,
deserialize_with = "deserialize_empty_string_as_none_datetime"
)]
pub password_change_date: Option<DateTime<Utc>>,
pub creator: String,
#[serde(rename = "siteName")]
pub site_name: String,
#[serde(rename = "siteUniqueId")]
pub site_unique_id: String,
#[serde(rename = "lastLanguage")]
pub last_language: Option<String>,
#[serde(rename = "numberOfForms")]
pub number_of_forms: usize,
pub forms: Option<Arc<Vec<Form>>>,
}
impl Patient {
pub(crate) fn from_attributes(e: &BytesStart<'_>) -> Result<Self, crate::errors::Error> {
let mut patient_id: Option<&str> = None;
let mut unique_id: Option<&str> = None;
let mut when_created = "";
let mut password_change_date = "";
let mut creator: Option<&str> = None;
let mut site_name: Option<&str> = None;
let mut site_unique_id: Option<&str> = None;
let mut last_language = "";
let mut number_of_forms = "";
visit_attributes(e, |key, attr| match key {
b"patientId" => patient_id = Some(attr),
b"uniqueId" => unique_id = Some(attr),
b"whenCreated" => when_created = attr,
b"passwordChangeDate" => password_change_date = attr,
b"creator" => creator = Some(attr),
b"siteName" => site_name = Some(attr),
b"siteUniqueId" => site_unique_id = Some(attr),
b"lastLanguage" => last_language = attr,
b"numberOfForms" => number_of_forms = attr,
_ => {}
})?;
Ok(Patient {
patient_id: required_attribute(patient_id, "patientId")?,
unique_id: required_attribute(unique_id, "uniqueId")?,
when_created: checked_datetime(when_created)?,
password_change_date: checked_datetime(password_change_date)?,
creator: required_attribute(creator, "creator")?,
site_name: required_attribute(site_name, "siteName")?,
site_unique_id: required_attribute(site_unique_id, "siteUniqueId")?,
last_language: optional_string(last_language),
number_of_forms: number_of_forms.parse().unwrap_or(0),
forms: None,
})
}
pub(crate) fn set_forms(&mut self, forms: Vec<Form>) {
self.forms = if forms.is_empty() {
None
} else {
Some(Arc::new(forms))
};
}
}
#[cfg(feature = "python")]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[pyclass(skip_from_py_object)]
pub struct Patient {
#[serde(rename = "patientId")]
#[serde(alias = "@patientId")]
#[serde(alias = "patientId")]
pub patient_id: String,
#[serde(rename = "uniqueId")]
#[serde(alias = "@uniqueId")]
#[serde(alias = "uniqueId")]
pub unique_id: String,
#[serde(rename = "whenCreated")]
#[serde(alias = "@whenCreated")]
#[serde(alias = "whenCreated")]
pub when_created: Option<DateTime<Utc>>,
#[serde(rename = "passwordChangeDate")]
#[serde(alias = "@passwordChangeDate")]
#[serde(alias = "passwordChangeDate")]
#[serde(
default,
deserialize_with = "deserialize_empty_string_as_none_datetime"
)]
pub password_change_date: Option<DateTime<Utc>>,
#[serde(rename = "creator")]
#[serde(alias = "@creator")]
#[serde(alias = "creator")]
pub creator: String,
#[serde(rename = "siteName")]
#[serde(alias = "@siteName")]
#[serde(alias = "siteName")]
pub site_name: String,
#[serde(rename = "siteUniqueId")]
#[serde(alias = "@siteUniqueId")]
#[serde(alias = "siteUniqueId")]
pub site_unique_id: String,
#[serde(rename = "lastLanguage")]
#[serde(alias = "@lastLanguage")]
#[serde(alias = "lastLanguage")]
#[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
pub last_language: Option<String>,
#[serde(rename = "numberOfForms")]
#[serde(alias = "@numberOfForms")]
#[serde(alias = "numberOfForms")]
pub number_of_forms: usize,
#[serde(alias = "form")]
pub forms: Option<Arc<Vec<Form>>>,
}
#[cfg(feature = "python")]
#[pymethods]
impl Patient {
#[getter]
fn patient_id(&self) -> PyResult<String> {
Ok(self.patient_id.clone())
}
#[getter]
fn unique_id(&self) -> PyResult<String> {
Ok(self.unique_id.clone())
}
#[getter]
fn when_created<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
self.when_created
.as_ref()
.map(|dt| to_py_datetime(py, dt))
.transpose()
}
#[getter]
fn password_change_date<'py>(
&self,
py: Python<'py>,
) -> PyResult<Option<Bound<'py, PyDateTime>>> {
self.password_change_date
.as_ref()
.map(|dt| to_py_datetime(py, dt))
.transpose()
}
#[getter]
fn creator(&self) -> PyResult<String> {
Ok(self.creator.clone())
}
#[getter]
fn site_name(&self) -> PyResult<String> {
Ok(self.site_name.clone())
}
#[getter]
fn site_unique_id(&self) -> PyResult<String> {
Ok(self.site_unique_id.clone())
}
#[getter]
fn last_language(&self) -> PyResult<Option<String>> {
Ok(self.last_language.clone())
}
#[getter]
fn number_of_forms(&self) -> PyResult<usize> {
Ok(self.number_of_forms)
}
#[getter]
fn forms(&self) -> PyResult<Option<Vec<Form>>> {
Ok(self.forms.as_deref().cloned())
}
pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let dict = PyDict::new(py);
dict.set_item("patient_id", &self.patient_id)?;
dict.set_item("unique_id", &self.unique_id)?;
dict.set_item(
"when_created",
self.when_created
.as_ref()
.map(|dt| to_py_datetime(py, dt))
.transpose()?,
)?;
dict.set_item(
"password_change_date",
self.password_change_date
.as_ref()
.map(|dt| to_py_datetime(py, dt))
.transpose()?,
)?;
dict.set_item("creator", &self.creator)?;
dict.set_item("site_name", &self.site_name)?;
dict.set_item("site_unique_id", &self.site_unique_id)?;
dict.set_item("last_language", &self.last_language)?;
dict.set_item("number_of_forms", self.number_of_forms)?;
let mut form_dicts = Vec::new();
if let Some(forms) = &self.forms {
for form in forms.iter() {
let form_dict = form.to_dict(py)?;
form_dicts.push(form_dict);
}
dict.set_item("forms", form_dicts)?;
} else {
dict.set_item("forms", py.None())?;
}
Ok(dict)
}
}
#[cfg(not(feature = "python"))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SubjectNative {
#[serde(default)]
pub export: Option<Export>,
pub patients: Vec<Patient>,
}
#[cfg(not(feature = "python"))]
impl SubjectNative {
pub fn to_json(&self) -> serde_json::Result<String> {
let json = serde_json::to_string(&self)?;
Ok(json)
}
}
#[cfg(feature = "python")]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[pyclass(get_all, skip_from_py_object)]
pub struct SubjectNative {
#[serde(default)]
pub export: Option<Export>,
#[serde(alias = "patient")]
pub patients: Vec<Patient>,
}
#[cfg(feature = "python")]
#[pymethods]
impl SubjectNative {
#[getter]
fn export(&self) -> PyResult<Option<Export>> {
Ok(self.export.clone())
}
#[getter]
fn patients(&self) -> PyResult<Vec<Patient>> {
Ok(self.patients.clone())
}
fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let dict = PyDict::new(py);
let mut patient_dicts = Vec::new();
for patient in &self.patients {
let patient_dict = patient.to_dict(py)?;
patient_dicts.push(patient_dict);
}
match &self.export {
Some(export) => dict.set_item("export", export.to_dict(py)?)?,
None => dict.set_item("export", py.None())?,
}
dict.set_item("patients", patient_dicts)?;
Ok(dict)
}
fn to_json(&self) -> PyResult<String> {
serde_json::to_string(&self)
.map_err(|_| PyErr::new::<PyValueError, _>("Error converting to JSON"))
}
}
#[cfg(test)]
mod tests {
use insta::assert_yaml_snapshot;
use super::*;
#[test]
fn deserialize_subject_native_json() {
let json_str = r#"{
"patients": [
{
"patientId": "ABC-001",
"uniqueId": "1681574905819",
"whenCreated": "2023-04-15T16:09:02Z",
"creator": "Paul Sanders",
"siteName": "Some Site",
"siteUniqueId": "1681574834910",
"lastLanguage": "English",
"numberOfForms": 6,
"forms": [
{
"name": "day.0.form.name.demographics",
"lastModified": "2023-04-15T16:09:15Z",
"whoLastModifiedName": "Paul Sanders",
"whoLastModifiedRole": "Project Manager",
"whenCreated": 1681574905839,
"hasErrors": false,
"hasWarnings": false,
"locked": false,
"user": null,
"dateTimeChanged": null,
"formTitle": "Demographics",
"formIndex": 1,
"formGroup": "Day 0",
"formState": "In-Work",
"states": [
{
"value": "form.state.in.work",
"signer": "Paul Sanders - Project Manager",
"signerUniqueId": "1681162687395",
"dateSigned": "2023-04-15T16:09:02Z"
}
],
"categories": [
{
"name": "Demographics",
"categoryType": "normal",
"highestIndex": 0,
"fields": [
{
"name": "breed",
"fieldType": "combo-box",
"dataType": "string",
"errorCode": "valid",
"whenCreated": "2023-04-15T16:08:26Z",
"keepHistory": true,
"entries": [
{
"entryId": "1",
"value": {
"by": "Paul Sanders",
"byUniqueId": "1681162687395",
"role": "Project Manager",
"when": "2023-04-15T16:09:02Z",
"value": "Labrador"
},
"reason": null
}
]
}
]
}
]
}
]
},
{
"patientId": "DEF-002",
"uniqueId": "1681574905820",
"whenCreated": "2023-04-16T16:10:02Z",
"creator": "Wade Watts",
"siteName": "Another Site",
"siteUniqueId": "1681574834911",
"lastLanguage": null,
"numberOfForms": 8,
"forms": [
{
"name": "day.0.form.name.demographics",
"lastModified": "2023-04-16T16:10:15Z",
"whoLastModifiedName": "Barney Rubble",
"whoLastModifiedRole": "Technician",
"whenCreated": 1681574905838,
"hasErrors": false,
"hasWarnings": false,
"locked": false,
"user": null,
"dateTimeChanged": null,
"formTitle": "Demographics",
"formIndex": 1,
"formGroup": "Day 0",
"formState": "In-Work",
"states": [
{
"value": "form.state.in.work",
"signer": "Paul Sanders - Project Manager",
"signerUniqueId": "1681162687395",
"dateSigned": "2023-04-16T16:10:02Z"
}
],
"categories": [
{
"name": "Demographics",
"categoryType": "normal",
"highestIndex": 0,
"fields": [
{
"name": "breed",
"fieldType": "combo-box",
"dataType": "string",
"errorCode": "valid",
"whenCreated": "2023-04-15T16:08:26Z",
"keepHistory": true,
"entries": [
{
"entryId": "1",
"value": {
"by": "Paul Sanders",
"byUniqueId": "1681162687395",
"role": "Project Manager",
"when": "2023-04-15T16:09:02Z",
"value": "Labrador"
},
"reason": null
}
]
}
]
}
]
}
]
}
]
}
"#;
let result: SubjectNative = serde_json::from_str(json_str).unwrap();
assert_yaml_snapshot!(result);
}
}