1use std::sync::Arc;
2
3use chrono::{DateTime, Utc};
4
5#[cfg(feature = "python")]
6use pyo3::{
7 exceptions::PyValueError,
8 prelude::*,
9 types::{PyDateTime, PyDict},
10};
11
12use quick_xml::events::BytesStart;
13
14use crate::native::deserializers::{
15 checked_datetime, deserialize_empty_string_as_none_datetime, optional_string,
16 required_attribute, visit_attributes,
17};
18
19#[cfg(feature = "python")]
20use crate::native::deserializers::{deserialize_empty_string_as_none, to_py_datetime};
21
22use serde::{Deserialize, Serialize};
23
24pub use crate::native::common::{
25 Category, Comment, Entry, Export, Field, Form, Reason, State, Value,
26};
27
28#[cfg(not(feature = "python"))]
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct Patient {
31 #[serde(rename = "patientId")]
32 pub patient_id: String,
33 #[serde(rename = "uniqueId")]
34 pub unique_id: String,
35 #[serde(rename = "whenCreated")]
36 pub when_created: Option<DateTime<Utc>>,
37 #[serde(rename = "passwordChangeDate")]
38 #[serde(
39 default,
40 deserialize_with = "deserialize_empty_string_as_none_datetime"
41 )]
42 pub password_change_date: Option<DateTime<Utc>>,
43 pub creator: String,
44 #[serde(rename = "siteName")]
45 pub site_name: String,
46 #[serde(rename = "siteUniqueId")]
47 pub site_unique_id: String,
48 #[serde(rename = "lastLanguage")]
49 pub last_language: Option<String>,
50 #[serde(rename = "numberOfForms")]
51 pub number_of_forms: usize,
52 pub forms: Option<Arc<Vec<Form>>>,
53}
54
55impl Patient {
56 pub(crate) fn from_attributes(e: &BytesStart<'_>) -> Result<Self, crate::errors::Error> {
57 let mut patient_id: Option<&str> = None;
58 let mut unique_id: Option<&str> = None;
59 let mut when_created = "";
60 let mut password_change_date = "";
61 let mut creator: Option<&str> = None;
62 let mut site_name: Option<&str> = None;
63 let mut site_unique_id: Option<&str> = None;
64 let mut last_language = "";
65 let mut number_of_forms = "";
66
67 visit_attributes(e, |key, attr| match key {
68 b"patientId" => patient_id = Some(attr),
69 b"uniqueId" => unique_id = Some(attr),
70 b"whenCreated" => when_created = attr,
71 b"passwordChangeDate" => password_change_date = attr,
72 b"creator" => creator = Some(attr),
73 b"siteName" => site_name = Some(attr),
74 b"siteUniqueId" => site_unique_id = Some(attr),
75 b"lastLanguage" => last_language = attr,
76 b"numberOfForms" => number_of_forms = attr,
77 _ => {}
78 })?;
79
80 Ok(Patient {
81 patient_id: required_attribute(patient_id, "patientId")?,
82 unique_id: required_attribute(unique_id, "uniqueId")?,
83 when_created: checked_datetime(when_created)?,
84 password_change_date: checked_datetime(password_change_date)?,
85 creator: required_attribute(creator, "creator")?,
86 site_name: required_attribute(site_name, "siteName")?,
87 site_unique_id: required_attribute(site_unique_id, "siteUniqueId")?,
88 last_language: optional_string(last_language),
89 number_of_forms: number_of_forms.parse().unwrap_or(0),
90 forms: None,
91 })
92 }
93
94 pub(crate) fn set_forms(&mut self, forms: Vec<Form>) {
95 self.forms = if forms.is_empty() {
96 None
97 } else {
98 Some(Arc::new(forms))
99 };
100 }
101}
102
103#[cfg(feature = "python")]
104#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
105#[serde(rename_all = "camelCase")]
106#[pyclass(skip_from_py_object)]
107pub struct Patient {
108 #[serde(rename = "patientId")]
109 #[serde(alias = "@patientId")]
110 #[serde(alias = "patientId")]
111 pub patient_id: String,
112 #[serde(rename = "uniqueId")]
113 #[serde(alias = "@uniqueId")]
114 #[serde(alias = "uniqueId")]
115 pub unique_id: String,
116 #[serde(rename = "whenCreated")]
117 #[serde(alias = "@whenCreated")]
118 #[serde(alias = "whenCreated")]
119 pub when_created: Option<DateTime<Utc>>,
120 #[serde(rename = "passwordChangeDate")]
121 #[serde(alias = "@passwordChangeDate")]
122 #[serde(alias = "passwordChangeDate")]
123 #[serde(
124 default,
125 deserialize_with = "deserialize_empty_string_as_none_datetime"
126 )]
127 pub password_change_date: Option<DateTime<Utc>>,
128
129 #[serde(rename = "creator")]
130 #[serde(alias = "@creator")]
131 #[serde(alias = "creator")]
132 pub creator: String,
133 #[serde(rename = "siteName")]
134 #[serde(alias = "@siteName")]
135 #[serde(alias = "siteName")]
136 pub site_name: String,
137 #[serde(rename = "siteUniqueId")]
138 #[serde(alias = "@siteUniqueId")]
139 #[serde(alias = "siteUniqueId")]
140 pub site_unique_id: String,
141
142 #[serde(rename = "lastLanguage")]
143 #[serde(alias = "@lastLanguage")]
144 #[serde(alias = "lastLanguage")]
145 #[serde(default, deserialize_with = "deserialize_empty_string_as_none")]
146 pub last_language: Option<String>,
147
148 #[serde(rename = "numberOfForms")]
149 #[serde(alias = "@numberOfForms")]
150 #[serde(alias = "numberOfForms")]
151 pub number_of_forms: usize,
152
153 #[serde(alias = "form")]
154 pub forms: Option<Arc<Vec<Form>>>,
155}
156
157#[cfg(feature = "python")]
158#[pymethods]
159impl Patient {
160 #[getter]
161 fn patient_id(&self) -> PyResult<String> {
162 Ok(self.patient_id.clone())
163 }
164
165 #[getter]
166 fn unique_id(&self) -> PyResult<String> {
167 Ok(self.unique_id.clone())
168 }
169
170 #[getter]
171 fn when_created<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDateTime>>> {
172 self.when_created
173 .as_ref()
174 .map(|dt| to_py_datetime(py, dt))
175 .transpose()
176 }
177
178 #[getter]
179 fn password_change_date<'py>(
180 &self,
181 py: Python<'py>,
182 ) -> PyResult<Option<Bound<'py, PyDateTime>>> {
183 self.password_change_date
184 .as_ref()
185 .map(|dt| to_py_datetime(py, dt))
186 .transpose()
187 }
188
189 #[getter]
190 fn creator(&self) -> PyResult<String> {
191 Ok(self.creator.clone())
192 }
193
194 #[getter]
195 fn site_name(&self) -> PyResult<String> {
196 Ok(self.site_name.clone())
197 }
198
199 #[getter]
200 fn site_unique_id(&self) -> PyResult<String> {
201 Ok(self.site_unique_id.clone())
202 }
203
204 #[getter]
205 fn last_language(&self) -> PyResult<Option<String>> {
206 Ok(self.last_language.clone())
207 }
208
209 #[getter]
210 fn number_of_forms(&self) -> PyResult<usize> {
211 Ok(self.number_of_forms)
212 }
213
214 #[getter]
215 fn forms(&self) -> PyResult<Option<Vec<Form>>> {
216 Ok(self.forms.as_deref().cloned())
217 }
218
219 pub fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
220 let dict = PyDict::new(py);
221 dict.set_item("patient_id", &self.patient_id)?;
222 dict.set_item("unique_id", &self.unique_id)?;
223 dict.set_item(
224 "when_created",
225 self.when_created
226 .as_ref()
227 .map(|dt| to_py_datetime(py, dt))
228 .transpose()?,
229 )?;
230 dict.set_item(
231 "password_change_date",
232 self.password_change_date
233 .as_ref()
234 .map(|dt| to_py_datetime(py, dt))
235 .transpose()?,
236 )?;
237 dict.set_item("creator", &self.creator)?;
238 dict.set_item("site_name", &self.site_name)?;
239 dict.set_item("site_unique_id", &self.site_unique_id)?;
240 dict.set_item("last_language", &self.last_language)?;
241 dict.set_item("number_of_forms", self.number_of_forms)?;
242
243 let mut form_dicts = Vec::new();
244 if let Some(forms) = &self.forms {
245 for form in forms.iter() {
246 let form_dict = form.to_dict(py)?;
247 form_dicts.push(form_dict);
248 }
249 dict.set_item("forms", form_dicts)?;
250 } else {
251 dict.set_item("forms", py.None())?;
252 }
253
254 Ok(dict)
255 }
256}
257
258#[cfg(not(feature = "python"))]
259#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
261pub struct SubjectNative {
262 #[serde(default)]
263 pub export: Option<Export>,
264 pub patients: Vec<Patient>,
265}
266
267#[cfg(not(feature = "python"))]
268impl SubjectNative {
269 pub fn to_json(&self) -> serde_json::Result<String> {
287 let json = serde_json::to_string(&self)?;
288
289 Ok(json)
290 }
291}
292
293#[cfg(feature = "python")]
294#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
296#[serde(rename_all = "camelCase")]
297#[pyclass(get_all, skip_from_py_object)]
298pub struct SubjectNative {
299 #[serde(default)]
300 pub export: Option<Export>,
301
302 #[serde(alias = "patient")]
303 pub patients: Vec<Patient>,
304}
305
306#[cfg(feature = "python")]
307#[pymethods]
308impl SubjectNative {
309 #[getter]
310 fn export(&self) -> PyResult<Option<Export>> {
311 Ok(self.export.clone())
312 }
313
314 #[getter]
315 fn patients(&self) -> PyResult<Vec<Patient>> {
316 Ok(self.patients.clone())
317 }
318
319 fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
321 let dict = PyDict::new(py);
322 let mut patient_dicts = Vec::new();
323 for patient in &self.patients {
324 let patient_dict = patient.to_dict(py)?;
325 patient_dicts.push(patient_dict);
326 }
327 match &self.export {
328 Some(export) => dict.set_item("export", export.to_dict(py)?)?,
329 None => dict.set_item("export", py.None())?,
330 }
331 dict.set_item("patients", patient_dicts)?;
332 Ok(dict)
333 }
334
335 fn to_json(&self) -> PyResult<String> {
337 serde_json::to_string(&self)
338 .map_err(|_| PyErr::new::<PyValueError, _>("Error converting to JSON"))
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use insta::assert_yaml_snapshot;
345
346 use super::*;
347
348 #[test]
349 fn deserialize_subject_native_json() {
350 let json_str = r#"{
351 "patients": [
352 {
353 "patientId": "ABC-001",
354 "uniqueId": "1681574905819",
355 "whenCreated": "2023-04-15T16:09:02Z",
356 "creator": "Paul Sanders",
357 "siteName": "Some Site",
358 "siteUniqueId": "1681574834910",
359 "lastLanguage": "English",
360 "numberOfForms": 6,
361 "forms": [
362 {
363 "name": "day.0.form.name.demographics",
364 "lastModified": "2023-04-15T16:09:15Z",
365 "whoLastModifiedName": "Paul Sanders",
366 "whoLastModifiedRole": "Project Manager",
367 "whenCreated": 1681574905839,
368 "hasErrors": false,
369 "hasWarnings": false,
370 "locked": false,
371 "user": null,
372 "dateTimeChanged": null,
373 "formTitle": "Demographics",
374 "formIndex": 1,
375 "formGroup": "Day 0",
376 "formState": "In-Work",
377 "states": [
378 {
379 "value": "form.state.in.work",
380 "signer": "Paul Sanders - Project Manager",
381 "signerUniqueId": "1681162687395",
382 "dateSigned": "2023-04-15T16:09:02Z"
383 }
384 ],
385 "categories": [
386 {
387 "name": "Demographics",
388 "categoryType": "normal",
389 "highestIndex": 0,
390 "fields": [
391 {
392 "name": "breed",
393 "fieldType": "combo-box",
394 "dataType": "string",
395 "errorCode": "valid",
396 "whenCreated": "2023-04-15T16:08:26Z",
397 "keepHistory": true,
398 "entries": [
399 {
400 "entryId": "1",
401 "value": {
402 "by": "Paul Sanders",
403 "byUniqueId": "1681162687395",
404 "role": "Project Manager",
405 "when": "2023-04-15T16:09:02Z",
406 "value": "Labrador"
407 },
408 "reason": null
409 }
410 ]
411 }
412 ]
413 }
414 ]
415 }
416 ]
417 },
418 {
419 "patientId": "DEF-002",
420 "uniqueId": "1681574905820",
421 "whenCreated": "2023-04-16T16:10:02Z",
422 "creator": "Wade Watts",
423 "siteName": "Another Site",
424 "siteUniqueId": "1681574834911",
425 "lastLanguage": null,
426 "numberOfForms": 8,
427 "forms": [
428 {
429 "name": "day.0.form.name.demographics",
430 "lastModified": "2023-04-16T16:10:15Z",
431 "whoLastModifiedName": "Barney Rubble",
432 "whoLastModifiedRole": "Technician",
433 "whenCreated": 1681574905838,
434 "hasErrors": false,
435 "hasWarnings": false,
436 "locked": false,
437 "user": null,
438 "dateTimeChanged": null,
439 "formTitle": "Demographics",
440 "formIndex": 1,
441 "formGroup": "Day 0",
442 "formState": "In-Work",
443 "states": [
444 {
445 "value": "form.state.in.work",
446 "signer": "Paul Sanders - Project Manager",
447 "signerUniqueId": "1681162687395",
448 "dateSigned": "2023-04-16T16:10:02Z"
449 }
450 ],
451 "categories": [
452 {
453 "name": "Demographics",
454 "categoryType": "normal",
455 "highestIndex": 0,
456 "fields": [
457 {
458 "name": "breed",
459 "fieldType": "combo-box",
460 "dataType": "string",
461 "errorCode": "valid",
462 "whenCreated": "2023-04-15T16:08:26Z",
463 "keepHistory": true,
464 "entries": [
465 {
466 "entryId": "1",
467 "value": {
468 "by": "Paul Sanders",
469 "byUniqueId": "1681162687395",
470 "role": "Project Manager",
471 "when": "2023-04-15T16:09:02Z",
472 "value": "Labrador"
473 },
474 "reason": null
475 }
476 ]
477 }
478 ]
479 }
480 ]
481 }
482 ]
483 }
484 ]
485}
486 "#;
487
488 let result: SubjectNative = serde_json::from_str(json_str).unwrap();
489
490 assert_yaml_snapshot!(result);
491 }
492}