1use crate::{
4 DataError, Extensions, Fingerprint, MyDuration, Score, Validate, ValidationError, emit_error,
5};
6use core::fmt;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use serde_with::skip_serializing_none;
10use std::{hash::Hasher, str::FromStr};
11
12#[skip_serializing_none]
16#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
17#[serde(deny_unknown_fields)]
18pub struct XResult {
19 score: Option<Score>,
20 success: Option<bool>,
21 completion: Option<bool>,
22 response: Option<String>,
23 duration: Option<MyDuration>,
24 extensions: Option<Extensions>,
25}
26
27impl XResult {
28 pub fn builder() -> XResultBuilder {
30 XResultBuilder::default()
31 }
32
33 pub fn score(&self) -> Option<&Score> {
36 self.score.as_ref()
37 }
38
39 pub fn success(&self) -> Option<bool> {
42 self.success
43 }
44
45 pub fn completion(&self) -> Option<bool> {
47 self.completion
48 }
49
50 pub fn response(&self) -> Option<&str> {
52 self.response.as_deref()
53 }
54
55 pub fn duration(&self) -> Option<&MyDuration> {
58 if self.duration.is_none() {
59 None
60 } else {
61 self.duration.as_ref()
63 }
64 }
65
66 pub fn duration_to_iso8601(&self) -> Option<String> {
68 self.duration.as_ref().map(|x| x.to_iso8601())
69 }
70
71 pub fn extensions(&self) -> Option<&Extensions> {
74 self.extensions.as_ref()
75 }
76}
77
78impl Fingerprint for XResult {
79 fn fingerprint<H: Hasher>(&self, state: &mut H) {
80 if self.score.is_some() {
81 self.score().unwrap().fingerprint(state)
82 }
83 if self.success.is_some() {
84 state.write_u8(if self.success().unwrap() { 1 } else { 0 })
85 }
86 if self.completion.is_some() {
87 state.write_u8(if self.completion().unwrap() { 1 } else { 0 })
88 }
89 if self.response.is_some() {
90 state.write(self.response().unwrap().as_bytes())
91 }
92 if self.duration.is_some() {
93 self.duration().unwrap().fingerprint(state);
94 }
95 if self.extensions.is_some() {
96 self.extensions().unwrap().fingerprint(state)
97 }
98 }
99}
100
101impl fmt::Display for XResult {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 let mut vec = vec![];
104
105 if let Some(z_score) = self.score.as_ref() {
106 vec.push(format!("score: {}", z_score))
107 }
108 if let Some(z_success) = self.success {
109 vec.push(format!("success? {}", z_success))
110 }
111 if let Some(z_completion) = self.completion {
112 vec.push(format!("completion? {}", z_completion))
113 }
114 if let Some(z_response) = self.response.as_ref() {
115 vec.push(format!("response: \"{}\"", z_response))
116 }
117 if self.duration.is_some() {
118 vec.push(format!(
119 "duration: \"{}\"",
120 self.duration_to_iso8601().unwrap()
121 ))
122 }
123 if let Some(z_extensions) = self.extensions.as_ref() {
124 vec.push(format!("extensions: {}", z_extensions))
125 }
126
127 let res = vec
128 .iter()
129 .map(|x| x.to_string())
130 .collect::<Vec<_>>()
131 .join(", ");
132 write!(f, "Result{{ {res} }}")
133 }
134}
135
136impl Validate for XResult {
137 fn validate(&self) -> Vec<ValidationError> {
138 let mut vec = vec![];
139
140 if let Some(z_score) = self.score.as_ref() {
141 vec.extend(z_score.validate())
142 };
143 if self.response.is_some() && self.response.as_ref().unwrap().is_empty() {
145 vec.push(ValidationError::Empty("response".into()))
146 }
147 vec
150 }
151}
152
153#[derive(Debug, Default)]
155pub struct XResultBuilder {
156 _score: Option<Score>,
157 _success: Option<bool>,
158 _completion: Option<bool>,
159 _response: Option<String>,
160 _duration: Option<MyDuration>,
161 _extensions: Option<Extensions>,
162}
163
164impl XResultBuilder {
165 pub fn score(mut self, val: Score) -> Result<Self, DataError> {
169 val.check_validity()?;
170 self._score = Some(val);
171 Ok(self)
172 }
173
174 pub fn success(mut self, val: bool) -> Self {
176 self._success = Some(val);
177 self
178 }
179
180 pub fn completion(mut self, val: bool) -> Self {
182 self._completion = Some(val);
183 self
184 }
185
186 pub fn response(mut self, val: &str) -> Result<Self, DataError> {
190 let val = val.trim();
191 if val.is_empty() {
192 emit_error!(DataError::Validation(ValidationError::Empty(
193 "response".into()
194 )))
195 } else {
196 self._response = Some(val.to_owned());
197 Ok(self)
198 }
199 }
200
201 pub fn duration(mut self, val: &str) -> Result<Self, DataError> {
205 let val = val.trim();
206 if val.is_empty() {
207 emit_error!(DataError::Validation(ValidationError::Empty(
208 "duration".into()
209 )))
210 } else {
211 self._duration = Some(MyDuration::from_str(val)?);
212 Ok(self)
213 }
214 }
215
216 pub fn extension(mut self, key: &str, value: &Value) -> Result<Self, DataError> {
218 if self._extensions.is_none() {
219 self._extensions = Some(Extensions::new());
220 }
221 let _ = self._extensions.as_mut().unwrap().add(key, value);
222 Ok(self)
223 }
224
225 pub fn with_extensions(mut self, map: Extensions) -> Result<Self, DataError> {
228 self._extensions = Some(map);
229 Ok(self)
230 }
231
232 pub fn build(self) -> Result<XResult, DataError> {
236 if self._score.is_none()
237 && self._success.is_none()
238 && self._completion.is_none()
239 && self._response.is_none()
240 && self._duration.is_none()
241 && self._extensions.is_none()
242 {
243 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
244 "At least one field must be set".into()
245 )))
246 } else {
247 Ok(XResult {
248 score: self._score,
249 success: self._success,
250 completion: self._completion,
251 response: self._response.as_ref().map(|x| x.to_string()),
252 duration: self._duration,
253 extensions: self._extensions,
254 })
255 }
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use iri_string::types::IriStr;
263 use std::str::FromStr;
264 use tracing_test::traced_test;
265
266 #[traced_test]
267 #[test]
268 fn test_simple() -> Result<(), DataError> {
269 const JSON: &str = r#"{
270 "extensions": {
271 "http://example.com/profiles/meetings/resultextensions/minuteslocation": "X:\\meetings\\minutes\\examplemeeting.one"
272 },
273 "success": true,
274 "completion": true,
275 "response": "We agreed on some example actions.",
276 "duration": "PT1H0M0S"
277 }"#;
278 let de_result = serde_json::from_str::<XResult>(JSON);
279 assert!(de_result.is_ok());
280 let res = de_result.unwrap();
281
282 assert!(res.success().is_some());
283 assert!(res.success().unwrap());
284 assert!(res.completion().is_some());
285 assert!(res.completion().unwrap());
286 assert!(res.response().is_some());
287 assert_eq!(
288 res.response().unwrap(),
289 "We agreed on some example actions."
290 );
291 assert!(res.duration().is_some());
292 let duration = MyDuration::from_str("PT1H0M0S").unwrap();
293 assert_eq!(res.duration().unwrap(), &duration);
294 assert!(res.extensions().is_some());
295 let exts = res.extensions().unwrap();
296
297 let iri =
298 IriStr::new("http://example.com/profiles/meetings/resultextensions/minuteslocation");
299 assert!(iri.is_ok());
300 let val = exts.get(iri.unwrap());
301 assert!(val.is_some());
302 assert_eq!(val.unwrap(), "X:\\meetings\\minutes\\examplemeeting.one");
303
304 Ok(())
305 }
306
307 #[traced_test]
308 #[test]
309 fn test_builder_w_duration() -> Result<(), DataError> {
310 const D: &str = "PT4H35M59.14S";
311
312 let res = XResult::builder().duration(D)?.build()?;
313
314 let d = res.duration().unwrap();
315 assert_eq!(d.second(), (4 * 60 * 60) + (35 * 60) + 59);
316 assert_eq!(d.microsecond(), 140 * 1_000);
317
318 Ok(())
319 }
320
321 #[traced_test]
322 #[test]
323 fn test_iso_duration() {
324 const D1: &str = "PT1H0M0S";
325 const D2: &str = "PT4H35M59.14S";
326
327 let res = MyDuration::from_str(D1);
328 assert!(res.is_ok());
329 let d = res.unwrap();
330 assert_eq!(d.second(), 60 * 60);
331 assert_eq!(d.microsecond(), 0);
332
333 let res = MyDuration::from_str(D2);
334 assert!(res.is_ok());
335 let d = res.unwrap();
336 assert_eq!(d.second(), (4 * 60 * 60) + (35 * 60) + 59);
337 assert_eq!(d.microsecond(), 140 * 1_000);
338 }
339
340 #[traced_test]
341 #[test]
342 fn test_iso_duration_fmt() {
343 const D1: &str = "PT1H0M0S";
344 let d1 = MyDuration::from_str(D1).unwrap();
345 assert_eq!(D1, d1.to_iso8601());
346 let d1_ = MyDuration::from_str("PT1H").unwrap();
347 assert_eq!(d1, d1_);
348
349 const D2: &str = "PT1H0M0.05S";
350 let d2 = MyDuration::from_str(D2).unwrap();
351 assert_eq!(D2, d2.to_iso8601());
352
353 const D3: &str = "PT1H0.0574S";
354 let d3 = MyDuration::from_str(D3).unwrap();
355 assert_eq!(d2.to_iso8601(), d3.to_iso8601());
357 assert_eq!(d2.second(), d3.second());
359 assert_eq!(d2.microsecond() / 10_000, d3.microsecond() / 10_000);
361 }
362
363 #[traced_test]
364 #[test]
365 fn test_iso_duration_truncated() -> Result<(), DataError> {
366 const D1: &str = "PT1H0.0574S";
367 const D2: &str = "PT1H0.05S";
368 const D3: &str = "PT1H0M0.05S";
369
370 let res = XResult::builder().duration(D1)?.build()?;
371 assert!(res.duration().is_some());
372 let d2 = MyDuration::from_str(D2).unwrap();
373 let d3 = MyDuration::from_str(D3).unwrap();
374 assert_eq!(d2, d3);
375 assert_eq!(res.duration_to_iso8601().unwrap(), d3.to_iso8601());
376
377 Ok(())
378 }
379
380 #[test]
381 #[should_panic]
382 fn test_duration_deserialization() {
383 const R: &str = r#"{
384 "score":{"scaled":0.95,"raw":95,"min":0,"max":100},
385 "extensions":{"http://example.com/profiles/meetings/resultextensions/minuteslocation":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one","http://example.com/profiles/meetings/resultextensions/reporter":{"name":"Thomas","id":"http://openid.com/342"}},
386 "success":true,
387 "completion":true,
388 "response":"We agreed on some example actions.",
389 "duration":"P4W1D"}"#;
390
391 serde_json::from_str::<XResult>(R).unwrap();
392 }
393}