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