Skip to main content

xapi_data/
statement.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::{
4    Actor, ActorId, Attachment, Context, ContextId, DataError, Fingerprint, MyTimestamp, MyVersion,
5    StatementObject, StatementObjectId, Validate, ValidationError, Verb, VerbId, XResult,
6    check_for_nulls, emit_error, fingerprint_it, statement_type::StatementType, stored_ser,
7};
8use chrono::{DateTime, SecondsFormat, Utc};
9use core::fmt;
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value};
12use serde_with::skip_serializing_none;
13use std::{hash::Hasher, str::FromStr};
14use uuid::Uuid;
15
16/// Structure showing evidence of any sort of experience or event to be tracked
17/// in xAPI as a _Learning Record_.
18///
19/// A set of several [Statement]s, each representing an event in time, might
20/// be used to track complete details about a _learning experience_.
21///
22#[skip_serializing_none]
23#[derive(Debug, Deserialize, PartialEq, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct Statement {
26    id: Option<Uuid>,
27    actor: Actor,
28    verb: Verb,
29    object: StatementObject,
30    result: Option<XResult>,
31    context: Option<Context>,
32    timestamp: Option<MyTimestamp>,
33    #[serde(serialize_with = "stored_ser")]
34    stored: Option<DateTime<Utc>>,
35    authority: Option<Actor>,
36    version: Option<MyVersion>,
37    attachments: Option<Vec<Attachment>>,
38}
39
40/// A doppelgänger structure that abides by the 'ids' format rules.
41/// Specifically all its elements are _identifiers_ when possible.
42#[skip_serializing_none]
43#[derive(Debug, Serialize)]
44#[doc(hidden)]
45pub struct StatementId {
46    id: Option<Uuid>,
47    actor: ActorId,
48    verb: VerbId,
49    object: StatementObjectId,
50    result: Option<XResult>,
51    context: Option<ContextId>,
52    timestamp: Option<MyTimestamp>,
53    #[serde(serialize_with = "stored_ser")]
54    stored: Option<DateTime<Utc>>,
55    authority: Option<ActorId>,
56    version: Option<MyVersion>,
57    attachments: Option<Vec<Attachment>>,
58}
59
60impl From<Statement> for StatementId {
61    fn from(value: Statement) -> Self {
62        StatementId {
63            id: value.id,
64            actor: ActorId::from(value.actor),
65            verb: value.verb.into(),
66            object: StatementObjectId::from(value.object),
67            result: value.result,
68            context: value.context.map(ContextId::from),
69            timestamp: value.timestamp,
70            stored: value.stored,
71            authority: value.authority.map(ActorId::from),
72            version: value.version,
73            attachments: value.attachments,
74        }
75    }
76}
77
78impl From<Box<Statement>> for StatementId {
79    fn from(value: Box<Statement>) -> Self {
80        StatementId {
81            id: value.id,
82            actor: ActorId::from(value.actor),
83            verb: value.verb.into(),
84            object: StatementObjectId::from(value.object),
85            result: value.result,
86            context: value.context.map(ContextId::from),
87            timestamp: value.timestamp,
88            stored: value.stored,
89            authority: value.authority.map(ActorId::from),
90            version: value.version,
91            attachments: value.attachments,
92        }
93    }
94}
95
96impl From<StatementId> for Statement {
97    fn from(value: StatementId) -> Self {
98        Statement {
99            id: value.id,
100            actor: Actor::from(value.actor),
101            verb: Verb::from(value.verb),
102            object: StatementObject::from(value.object),
103            result: value.result,
104            context: value.context.map(Context::from),
105            timestamp: value.timestamp,
106            stored: value.stored,
107            authority: value.authority.map(Actor::from),
108            version: value.version,
109            attachments: value.attachments,
110        }
111    }
112}
113
114impl From<Box<StatementId>> for Statement {
115    fn from(value: Box<StatementId>) -> Self {
116        Statement {
117            id: value.id,
118            actor: Actor::from(value.actor),
119            verb: Verb::from(value.verb),
120            object: StatementObject::from(value.object),
121            result: value.result,
122            context: value.context.map(Context::from),
123            timestamp: value.timestamp,
124            stored: value.stored,
125            authority: value.authority.map(Actor::from),
126            version: value.version,
127            attachments: value.attachments,
128        }
129    }
130}
131
132impl Statement {
133    /// Construct and validate a [Statement] from a JSON map.
134    pub fn from_json_obj(map: Map<String, Value>) -> Result<Self, DataError> {
135        for (k, v) in &map {
136            // NOTE (rsn) 20241104 - from "4.2.1 Table Guidelines": "The LRS
137            // shall reject Statements with any null values (except inside
138            // extensions)."
139            if v.is_null() {
140                emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
141                    format!("Key '{k}' is null").into()
142                )))
143            } else if k != "extensions" {
144                check_for_nulls(v)?
145            }
146        }
147        // finally convert it to a Statement...
148        let stmt: Statement = serde_json::from_value(Value::Object(map.to_owned()))?;
149        stmt.check_validity()?;
150        Ok(stmt)
151    }
152
153    /// Return a [Statement] _Builder_.
154    pub fn builder() -> StatementBuilder {
155        StatementBuilder::default()
156    }
157
158    /// Return the `id` field (a UUID) if set; `None` otherwise. It's assigned by
159    /// the LRS if not already set by the LRP.
160    pub fn id(&self) -> Option<&Uuid> {
161        self.id.as_ref()
162    }
163
164    /// Set the `id` field of this instance to the given value.
165    pub fn set_id(&mut self, id: Uuid) {
166        self.id = Some(id)
167    }
168
169    /// Return the [Actor] whom the [Statement] is about. The [Actor] is either
170    /// an [Agent][1] or a [Group][2].
171    ///
172    /// [1]: crate::Agent
173    /// [2]: crate::Group
174    pub fn actor(&self) -> &Actor {
175        &self.actor
176    }
177
178    /// Return the _action_ taken by the _actor_.
179    pub fn verb(&self) -> &Verb {
180        &self.verb
181    }
182
183    /// Return TRUE if `verb` is _voided_; FALSE otherwise.
184    pub fn is_verb_voided(&self) -> bool {
185        self.verb.is_voided()
186    }
187
188    /// Return an [Activity][1], an [Agent][2], or another [Statement] that is
189    /// the [Object][StatementObject] of this instance.
190    ///
191    /// [1]: crate::Activity
192    /// [2]: crate::Agent
193    pub fn object(&self) -> &StatementObject {
194        &self.object
195    }
196
197    /// Return the UUID of the (target) Statement to be voided by this one iff
198    /// (a) the verb is _voided_, and (b) the object is a [StatementRef][crate::StatementRef].
199    ///
200    /// Return `None` otherwise.
201    pub fn voided_target(&self) -> Option<Uuid> {
202        if self.is_verb_voided() && self.object.is_statement_ref() {
203            Some(
204                *self
205                    .object
206                    .as_statement_ref()
207                    .expect("Failed coercing object to StatementRef")
208                    .id(),
209            )
210        } else {
211            None
212        }
213    }
214
215    /// Return the [XResult] instance if set; `None` otherwise.
216    pub fn result(&self) -> Option<&XResult> {
217        self.result.as_ref()
218    }
219
220    /// Return the [Context] of this instance if set; `None` otherwise.
221    pub fn context(&self) -> Option<&Context> {
222        self.context.as_ref()
223    }
224
225    /// Return the timestamp of when the events described within this [Statement]
226    /// occurred as a `chrono::DateTime` if set; `None`  otherwise.
227    ///
228    /// It's set by the LRS if not provided.
229    pub fn timestamp(&self) -> Option<&DateTime<Utc>> {
230        if let Some(z_timestamp) = self.timestamp.as_ref() {
231            Some(z_timestamp.inner())
232        } else {
233            None
234        }
235    }
236
237    /// Return the timestamp of when the events described within this [Statement]
238    /// occurred if set; `None` otherwise.
239    ///
240    /// It's set by the LRS if not provided.
241    pub fn timestamp_internal(&self) -> Option<&MyTimestamp> {
242        self.timestamp.as_ref()
243    }
244
245    /// Return the timestamp of when this [Statement] was persisted if set;
246    /// `None` otherwise.
247    pub fn stored(&self) -> Option<&DateTime<Utc>> {
248        self.stored.as_ref()
249    }
250
251    /// Set the `stored` field of this to the given value.
252    pub fn set_stored(&mut self, val: DateTime<Utc>) {
253        self.stored = Some(val);
254    }
255
256    /// Return the [Agent][crate::Agent] or the [Group][crate::Group] who is
257    /// asserting this [Statement] is TRUE if set or `None` otherwise.
258    ///
259    /// When provided it should be verified by the LRS based on authentication.
260    /// It's set by LRS if not provided, or if a strong trust relationship
261    /// between the LRP and LRS has not been established.
262    pub fn authority(&self) -> Option<&Actor> {
263        self.authority.as_ref()
264    }
265
266    /// Setter for the `authority` field of this w/o checking the given
267    /// [Actor]'s validity leaving the caller that responsibility.
268    pub fn set_authority_unchecked(&mut self, actor: Actor) {
269        self.authority = Some(actor)
270    }
271
272    /// Return the [Statement]'s associated xAPI version if set; `None` otherwise.
273    ///
274    /// When set, it's expected to be formatted according to [Semantic Versioning
275    /// 1.0.0][1].
276    ///
277    /// [1]: https://semver.org/spec/v1.0.0.html
278    pub fn version(&self) -> Option<&MyVersion> {
279        if let Some(z_version) = self.version.as_ref() {
280            Some(z_version)
281        } else {
282            None
283        }
284    }
285
286    /// Return a reference to the potentially empty array of [`attachments`][Attachment].
287    pub fn attachments(&self) -> &[Attachment] {
288        match &self.attachments {
289            Some(x) => x,
290            None => &[],
291        }
292    }
293
294    /// Return a mutable reference to the potentially empty array of [`attachments`][Attachment].
295    pub fn attachments_mut(&mut self) -> &mut [Attachment] {
296        if self.attachments.is_some() {
297            self.attachments.as_deref_mut().unwrap()
298        } else {
299            &mut []
300        }
301    }
302
303    /// Set (as in replace) `attachments` field of this instance to the given
304    /// value.
305    pub fn set_attachments(&mut self, attachments: Vec<Attachment>) {
306        self.attachments = Some(attachments)
307    }
308
309    /// Return a pretty-printed output of `self`.
310    pub fn print(&self) -> String {
311        serde_json::to_string_pretty(self).unwrap_or_else(|_| String::from("$Statement"))
312    }
313
314    /// Return the fingerprint of this instance.
315    pub fn uid(&self) -> u64 {
316        fingerprint_it(self)
317    }
318
319    /// Return TRUE if this is _Equivalent_ to `that` and FALSE otherwise.
320    pub fn equivalent(&self, that: &Statement) -> bool {
321        self.uid() == that.uid()
322    }
323}
324
325impl StatementId {
326    #[allow(dead_code)]
327    pub(crate) fn stored(&self) -> Option<&DateTime<Utc>> {
328        self.stored.as_ref()
329    }
330
331    #[allow(dead_code)]
332    pub(crate) fn attachments(&self) -> &[Attachment] {
333        match &self.attachments {
334            Some(x) => x,
335            None => &[],
336        }
337    }
338}
339
340impl Fingerprint for Statement {
341    #[allow(clippy::let_unit_value)]
342    fn fingerprint<H: Hasher>(&self, state: &mut H) {
343        // discard `id`, `timestamp`, `stored`, `authority`, `version` and `attachments`
344        self.actor.fingerprint(state);
345        self.verb.fingerprint(state);
346        self.object.fingerprint(state);
347        let _ = self.context().map_or((), |x| x.fingerprint(state));
348        let _ = self.result().map_or((), |x| x.fingerprint(state));
349    }
350}
351
352impl fmt::Display for Statement {
353    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354        let mut vec = vec![];
355
356        if self.id().is_some() {
357            // always use the hyphenated lowercase format for UUIDs...
358            vec.push(format!(
359                "id: \"{}\"",
360                self.id
361                    .as_ref()
362                    .unwrap()
363                    .hyphenated()
364                    .encode_lower(&mut Uuid::encode_buffer())
365            ));
366        }
367        vec.push(format!("actor: {}", self.actor));
368        vec.push(format!("verb: {}", self.verb));
369        vec.push(format!("object: {}", self.object));
370        if let Some(z_result) = self.result.as_ref() {
371            vec.push(format!("result: {}", z_result))
372        }
373        if let Some(z_context) = self.context.as_ref() {
374            vec.push(format!("context: {}", z_context))
375        }
376        if let Some(z_timestamp) = self.timestamp.as_ref() {
377            vec.push(format!("timestamp: \"{}\"", z_timestamp))
378        }
379        if let Some(ts) = self.stored.as_ref() {
380            vec.push(format!(
381                "stored: \"{}\"",
382                ts.to_rfc3339_opts(SecondsFormat::Millis, true)
383            ))
384        }
385        if let Some(z_authority) = self.authority.as_ref() {
386            vec.push(format!("authority: {}", z_authority))
387        }
388        if let Some(z_version) = self.version.as_ref() {
389            vec.push(format!("version: \"{}\"", z_version))
390        }
391        if self.attachments.is_some() {
392            let items = self.attachments.as_deref().unwrap();
393            vec.push(format!(
394                "attachments: [{}]",
395                items
396                    .iter()
397                    .map(|x| x.to_string())
398                    .collect::<Vec<_>>()
399                    .join(", ")
400            ))
401        }
402        let res = vec
403            .iter()
404            .map(|x| x.to_string())
405            .collect::<Vec<_>>()
406            .join(", ");
407        write!(f, "Statement{{ {res} }}")
408    }
409}
410
411impl Validate for Statement {
412    /// IMPORTANT - xAPI mandates that... The LRS shall specifically not consider
413    /// any of the following for equivalence, nor is it responsible for preservation
414    /// as described above for the following properties/cases:
415    ///
416    /// * Case (upper vs. lower)
417    /// * Id
418    /// * Order of any Group Members
419    /// * Authority
420    /// * Stored
421    /// * Timestamp
422    /// * Version
423    /// * Any attachments
424    /// * Any referenced Activity Definitions
425    ///
426    fn validate(&self) -> Vec<ValidationError> {
427        let mut vec = vec![];
428
429        if self.id.is_some()
430            && (self.id.as_ref().unwrap().is_nil() || self.id.as_ref().unwrap().is_max())
431        {
432            vec.push(ValidationError::ConstraintViolation(
433                "'id' must not be all 0's or 1's".into(),
434            ))
435        }
436        vec.extend(self.actor.validate());
437        vec.extend(self.verb.validate());
438        vec.extend(self.object.validate());
439        if let Some(z_result) = self.result.as_ref() {
440            vec.extend(z_result.validate())
441        }
442        if let Some(z_context) = self.context.as_ref() {
443            vec.extend(z_context.validate());
444            // NOTE (rsn) 20241017 - pending a resolution to [1] i'm adding checks
445            // here to conform to the requirement that...
446            // > A Statement cannot contain both a "revision" property in its
447            // "context" property and have the value of the "object" property's
448            // "objectType" be anything but "Activity"
449            //
450            // [1]: https://github.com/adlnet/lrs-conformance-test-suite/issues/278
451            //
452            if !self.object().is_activity()
453                && (self.context().as_ref().unwrap().revision().is_some()
454                    || self.context().as_ref().unwrap().platform().is_some())
455            {
456                vec.push(ValidationError::ConstraintViolation(
457                    "Statement context w/ revision | platform but object != Activity".into(),
458                ))
459            }
460        }
461        if let Some(z_authority) = self.authority.as_ref() {
462            vec.extend(z_authority.validate());
463
464            // NOTE (rsn) 20241018 - Current v2_0 conformance tests apply v1_0_3
465            // constraints specified [here][1]. For `authority` these are:
466            // * XAPI-00098 - An `authority` property which is also a _Group_
467            //   contains exactly two _Agent_s. The LRS rejects with **`400 Bad
468            //   Request`**` a statement which has an `authority` property with
469            //   an `objectType` of `Group` with more or less than 2 Oauth
470            //   Agents as values of the `member` property.
471            // * XAPI-00099 - An LRS populates the `authority` property if it
472            //   is not provided in the _Statement_.
473            // * XAPI-00100 - An LRS rejects with error code **`400 Bad Request`**,
474            //   a Request whose `authority` is a _Group_ having more than two
475            //   _Agent_s.
476            // For now ensure the first and last ones are honored here. The
477            // middle one will be taken care of the same way `stored` is.
478            //
479            // [1]: https://adl.gitbooks.io/xapi-lrs-conformance-requirements/content/
480            if z_authority.is_group() {
481                let group = z_authority.as_group().unwrap();
482                if !group.is_anonymous() {
483                    vec.push(ValidationError::ConstraintViolation(
484                        "When used as an Authority, A Group must be anonymous".into(),
485                    ))
486                }
487                if group.members().len() != 2 {
488                    vec.push(ValidationError::ConstraintViolation(
489                        "When used as an Authority, an anonymous Group must have 2 members only"
490                            .into(),
491                    ))
492                }
493            }
494        }
495        if let Some(z_version) = self.version.as_ref() {
496            vec.extend(z_version.validate())
497        }
498        if let Some(z_attachments) = self.attachments.as_ref() {
499            for att in z_attachments.iter() {
500                vec.extend(att.validate())
501            }
502        }
503
504        vec
505    }
506}
507
508impl FromStr for Statement {
509    type Err = DataError;
510
511    fn from_str(s: &str) -> Result<Self, Self::Err> {
512        let map: Map<String, Value> = serde_json::from_str(s)?;
513        Self::from_json_obj(map)
514    }
515}
516
517impl TryFrom<StatementType> for Statement {
518    type Error = DataError;
519
520    fn try_from(value: StatementType) -> Result<Self, Self::Error> {
521        match value {
522            StatementType::S(x) => Ok(*x),
523            StatementType::SId(x) => Ok(Statement::from(x)),
524            _ => Err(DataError::Validation(ValidationError::ConstraintViolation(
525                "Not a Statement".into(),
526            ))),
527        }
528    }
529}
530
531impl TryFrom<StatementType> for StatementId {
532    type Error = DataError;
533
534    fn try_from(value: StatementType) -> Result<Self, Self::Error> {
535        match value {
536            StatementType::S(x) => Ok(StatementId::from(x)),
537            StatementType::SId(x) => Ok(*x),
538            _ => Err(DataError::Validation(ValidationError::ConstraintViolation(
539                "Not a StatementId".into(),
540            ))),
541        }
542    }
543}
544
545/// A Type that knows how to construct [Statement].
546#[derive(Debug, Default)]
547pub struct StatementBuilder {
548    _id: Option<Uuid>,
549    _actor: Option<Actor>,
550    _verb: Option<Verb>,
551    _object: Option<StatementObject>,
552    _result: Option<XResult>,
553    _context: Option<Context>,
554    _timestamp: Option<MyTimestamp>,
555    _stored: Option<DateTime<Utc>>,
556    _authority: Option<Actor>,
557    _version: Option<MyVersion>,
558    _attachments: Option<Vec<Attachment>>,
559}
560
561impl StatementBuilder {
562    /// Set the `id` field parsing the argument as a UUID.
563    ///
564    /// Raise [DataError] if argument is empty, cannot be parsed into a
565    /// valid UUID, or is all zeroes (`nil` UUID) or ones (`max` UUID).
566    pub fn id(self, val: &str) -> Result<Self, DataError> {
567        let val = val.trim();
568        if val.is_empty() {
569            emit_error!(DataError::Validation(ValidationError::Empty("id".into())))
570        } else {
571            let uuid = Uuid::parse_str(val)?;
572            self.id_as_uuid(uuid)
573        }
574    }
575
576    /// Set the `id` field from given UUID.
577    ///
578    /// Raise [DataError] if argument is empty, or is all zeroes (`nil` UUID)
579    /// or ones (`max` UUID).
580    pub fn id_as_uuid(mut self, uuid: Uuid) -> Result<Self, DataError> {
581        if uuid.is_nil() || uuid.is_max() {
582            emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
583                "'id' should not be all 0's or 1's".into()
584            )))
585        } else {
586            self._id = Some(uuid);
587            Ok(self)
588        }
589    }
590
591    /// Set the `actor` field.
592    ///
593    /// Raise [DataError] if the argument is invalid.
594    pub fn actor(mut self, val: Actor) -> Result<Self, DataError> {
595        val.check_validity()?;
596        self._actor = Some(val);
597        Ok(self)
598    }
599
600    /// Set the `verb` field.
601    ///
602    /// Raise [DataError] if the argument is invalid.
603    pub fn verb(mut self, val: Verb) -> Result<Self, DataError> {
604        val.check_validity()?;
605        self._verb = Some(val);
606        Ok(self)
607    }
608
609    /// Set the `object` field.
610    ///
611    /// Raise [DataError] if the argument is invalid.
612    pub fn object(mut self, val: StatementObject) -> Result<Self, DataError> {
613        val.check_validity()?;
614        self._object = Some(val);
615        Ok(self)
616    }
617
618    /// Set the `result` field.
619    ///
620    /// Raise [DataError] if the argument is invalid.
621    pub fn result(mut self, val: XResult) -> Result<Self, DataError> {
622        val.check_validity()?;
623        self._result = Some(val);
624        Ok(self)
625    }
626
627    /// Set the `context` field.
628    ///
629    /// Raise [DataError] if the argument is invalid.
630    pub fn context(mut self, val: Context) -> Result<Self, DataError> {
631        val.check_validity()?;
632        self._context = Some(val);
633        Ok(self)
634    }
635
636    /// Set the `timestamp` field from a string.
637    ///
638    /// Raise [DataError] if the argument is empty or invalid.
639    pub fn timestamp(mut self, val: &str) -> Result<Self, DataError> {
640        let val = val.trim();
641        if val.is_empty() {
642            emit_error!(DataError::Validation(ValidationError::Empty(
643                "timestamp".into()
644            )))
645        }
646        let ts = MyTimestamp::from_str(val)?;
647        self._timestamp = Some(ts);
648        Ok(self)
649    }
650
651    /// Set the `timestamp` field from a [DateTime] value.
652    pub fn with_timestamp(mut self, val: DateTime<Utc>) -> Self {
653        self._timestamp = Some(MyTimestamp::from(val));
654        self
655    }
656
657    /// Set the `stored` field from a string.
658    ///
659    /// Raise [DataError] if the argument is empty or invalid.
660    pub fn stored(mut self, val: &str) -> Result<Self, DataError> {
661        let val = val.trim();
662        if val.is_empty() {
663            emit_error!(DataError::Validation(ValidationError::Empty(
664                "stored".into()
665            )))
666        }
667        let ts = serde_json::from_str::<MyTimestamp>(val)?;
668        self._stored = Some(*ts.inner());
669        Ok(self)
670    }
671
672    /// Set the `stored` field from a [DateTime] value.
673    pub fn with_stored(mut self, val: DateTime<Utc>) -> Self {
674        self._stored = Some(val);
675        self
676    }
677
678    /// Set the `authority` field.
679    ///
680    /// Raise [DataError] if the argument is invalid.
681    pub fn authority(mut self, val: Actor) -> Result<Self, DataError> {
682        val.check_validity()?;
683        // in addition it must satisfy the following constraints for
684        // use as an Authority --see validate():
685        if val.is_group() {
686            let group = val.as_group().unwrap();
687            if !group.is_anonymous() {
688                emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
689                    "When used as an Authority, a Group must be anonymous".into()
690                )))
691            }
692            if group.members().len() != 2 {
693                emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
694                    "When used as an Authority, an anonymous Group must have 2 members only".into()
695                )))
696            }
697        }
698        self._authority = Some(val);
699        Ok(self)
700    }
701
702    /// Set the `version` field.
703    ///
704    /// Raise [DataError] if the argument is empty or invalid.
705    pub fn version(mut self, val: &str) -> Result<Self, DataError> {
706        self._version = Some(MyVersion::from_str(val)?);
707        Ok(self)
708    }
709
710    /// Add `att` to `attachments` field if valid; otherwise raise a
711    /// [DataError].
712    pub fn attachment(mut self, att: Attachment) -> Result<Self, DataError> {
713        att.check_validity()?;
714        if self._attachments.is_none() {
715            self._attachments = Some(vec![])
716        }
717        self._attachments.as_mut().unwrap().push(att);
718        Ok(self)
719    }
720
721    /// Create a [Statement] from set field values.
722    ///
723    /// Raise [DataError] if an error occurs.
724    pub fn build(self) -> Result<Statement, DataError> {
725        if self._actor.is_none() || self._verb.is_none() || self._object.is_none() {
726            emit_error!(DataError::Validation(ValidationError::MissingField(
727                "actor, verb, or object".into()
728            )))
729        }
730        Ok(Statement {
731            id: self._id,
732            actor: self._actor.unwrap(),
733            verb: self._verb.unwrap(),
734            object: self._object.unwrap(),
735            result: self._result,
736            context: self._context,
737            timestamp: self._timestamp,
738            stored: self._stored,
739            authority: self._authority,
740            version: self._version,
741            attachments: self._attachments,
742        })
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749    use serde_json::{Map, Value};
750    use tracing_test::traced_test;
751
752    #[traced_test]
753    #[test]
754    #[should_panic]
755    fn test_extra_properties() {
756        const S: &str = r#"{
757"actor":{"objectType":"Agent","name":"xAPI mbox","mbox":"mailto:xapi@adlnet.gov"},
758"verb":{"id":"http://adlnet.gov/expapi/verbs/attended","display":{"en-US":"attended"}},
759"object":{"objectType":"Activity","id":"http://www.example.com/meetings/occurances/34534"},
760"iD":"46bf512f-56ec-45ef-8f95-1f4b352386e6"}"#;
761
762        let map: Map<String, Value> = serde_json::from_str(S).unwrap();
763        assert!(!map.contains_key("id"));
764        assert!(!map.contains_key("ID"));
765        assert!(!map.contains_key("Id"));
766        assert!(map.contains_key("iD"));
767        let s = serde_json::from_value::<Statement>(Value::Object(map));
768        assert!(s.is_err());
769
770        // now try from_str; which calls from_json_obj... it should panic
771        Statement::from_str(S).unwrap();
772    }
773
774    #[traced_test]
775    #[test]
776    fn test_extensions_w_nulls() {
777        const S: &str = r#"{
778"actor":{"objectType":"Agent","name":"xAPI account","mbox":"mailto:xapi@adlnet.gov"},
779"verb":{"id":"http://adlnet.gov/expapi/verbs/attended","display":{"en-GB":"attended"}},
780"object":{
781  "objectType":"Activity",
782  "id":"http://www.example.com/meetings/occurances/34534",
783  "definition":{
784    "type":"http://adlnet.gov/expapi/activities/meeting",
785    "name":{"en-GB":"example meeting","en-US":"example meeting"},
786    "description":{"en-GB":"An example meeting.","en-US":"An example meeting."},
787    "moreInfo":"http://virtualmeeting.example.com/345256",
788    "extensions":{"http://example.com/null":null}}}}"#;
789
790        assert!(Statement::from_str(S).is_ok());
791    }
792
793    #[test]
794    #[should_panic]
795    fn test_bad_duration() {
796        const S: &str = r#"{
797"actor":{"objectType":"Agent","name":"xAPI account","mbox":"mailto:xapi@adlnet.gov"},
798"verb":{"id":"http://adlnet.gov/expapi/verbs/attended","display":{"en-US":"attended"}},
799"result":{
800  "score":{"scaled":0.95,"raw":95,"min":0,"max":100},
801  "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"}},
802  "success":true,
803  "completion":true,
804  "response":"We agreed on some example actions.",
805  "duration":"P4W1D"},
806"object":{"objectType":"Activity","id":"http://www.example.com/meetings/occurances/34534"}}"#;
807
808        Statement::from_str(S).unwrap();
809    }
810}