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