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