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