Skip to main content

xapi_rs/data/
statement.rs

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