1use 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#[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#[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 pub fn from_json_obj(map: Map<String, Value>) -> Result<Self, DataError> {
137 for (k, v) in &map {
138 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 let stmt: Statement = serde_json::from_value(Value::Object(map.to_owned()))?;
151 stmt.check_validity()?;
152 Ok(stmt)
153 }
154
155 pub fn builder() -> StatementBuilder {
157 StatementBuilder::default()
158 }
159
160 pub fn id(&self) -> Option<&Uuid> {
163 self.id.as_ref()
164 }
165
166 pub fn set_id(&mut self, id: Uuid) {
168 self.id = Some(id)
169 }
170
171 pub fn actor(&self) -> &Actor {
177 &self.actor
178 }
179
180 pub fn verb(&self) -> &Verb {
182 &self.verb
183 }
184
185 pub fn is_verb_voided(&self) -> bool {
187 self.verb.is_voided()
188 }
189
190 pub fn object(&self) -> &StatementObject {
196 &self.object
197 }
198
199 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 pub fn result(&self) -> Option<&XResult> {
219 self.result.as_ref()
220 }
221
222 pub fn context(&self) -> Option<&Context> {
224 self.context.as_ref()
225 }
226
227 pub fn timestamp(&self) -> Option<&DateTime<Utc>> {
232 if self.timestamp.is_none() {
233 None
234 } else {
235 Some(self.timestamp.as_ref().unwrap().inner())
236 }
237 }
238
239 pub fn timestamp_internal(&self) -> Option<&MyTimestamp> {
244 self.timestamp.as_ref()
245 }
246
247 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 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 pub fn version(&self) -> Option<&MyVersion> {
278 if self.version.is_none() {
279 None
280 } else {
281 Some(self.version.as_ref().unwrap())
282 }
283 }
284
285 pub fn attachments(&self) -> &[Attachment] {
287 match &self.attachments {
288 Some(x) => x,
289 None => &[],
290 }
291 }
292
293 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 pub fn set_attachments(&mut self, attachments: Vec<Attachment>) {
305 self.attachments = Some(attachments)
306 }
307
308 pub fn print(&self) -> String {
310 serde_json::to_string_pretty(self).unwrap_or_else(|_| String::from("$Statement"))
311 }
312
313 pub fn uid(&self) -> u64 {
315 fingerprint_it(self)
316 }
317
318 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 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 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 self.result.is_some() {
368 vec.push(format!("result: {}", self.result.as_ref().unwrap()))
369 }
370 if self.context.is_some() {
371 vec.push(format!("context: {}", self.context.as_ref().unwrap()))
372 }
373 if self.timestamp.is_some() {
374 vec.push(format!(
375 "timestamp: \"{}\"",
376 self.timestamp.as_ref().unwrap()
377 ))
378 }
379 if self.stored.is_some() {
380 let ts = self.stored.as_ref().unwrap();
381 vec.push(format!(
382 "stored: \"{}\"",
383 ts.to_rfc3339_opts(SecondsFormat::Millis, true)
384 ))
385 }
386 if self.authority.is_some() {
387 vec.push(format!("authority: {}", self.authority.as_ref().unwrap()))
388 }
389 if self.version.is_some() {
390 vec.push(format!("version: \"{}\"", self.version.as_ref().unwrap()))
391 }
392 if self.attachments.is_some() {
393 let items = self.attachments.as_deref().unwrap();
394 vec.push(format!(
395 "attachments: [{}]",
396 items
397 .iter()
398 .map(|x| x.to_string())
399 .collect::<Vec<_>>()
400 .join(", ")
401 ))
402 }
403 let res = vec
404 .iter()
405 .map(|x| x.to_string())
406 .collect::<Vec<_>>()
407 .join(", ");
408 write!(f, "Statement{{ {res} }}")
409 }
410}
411
412impl Validate for Statement {
413 fn validate(&self) -> Vec<ValidationError> {
428 let mut vec = vec![];
429
430 if self.id.is_some()
431 && (self.id.as_ref().unwrap().is_nil() || self.id.as_ref().unwrap().is_max())
432 {
433 vec.push(ValidationError::ConstraintViolation(
434 "'id' must not be all 0's or 1's".into(),
435 ))
436 }
437 vec.extend(self.actor.validate());
438 vec.extend(self.verb.validate());
439 vec.extend(self.object.validate());
440 if self.result.is_some() {
441 vec.extend(self.result.as_ref().unwrap().validate())
442 }
443 if self.context.is_some() {
444 vec.extend(self.context.as_ref().unwrap().validate());
445 if !self.object().is_activity()
454 && (self.context().as_ref().unwrap().revision().is_some()
455 || self.context().as_ref().unwrap().platform().is_some())
456 {
457 vec.push(ValidationError::ConstraintViolation(
458 "Statement context w/ revision | platform but object != Activity".into(),
459 ))
460 }
461 }
462 if self.authority.is_some() {
463 vec.extend(self.authority.as_ref().unwrap().validate());
464
465 if self.authority.as_ref().unwrap().is_group() {
482 let group = self.authority.as_ref().unwrap().as_group().unwrap();
483 if !group.is_anonymous() {
484 vec.push(ValidationError::ConstraintViolation(
485 "When used as an Authority, A Group must be anonymous".into(),
486 ))
487 }
488 if group.members().len() != 2 {
489 vec.push(ValidationError::ConstraintViolation(
490 "When used as an Authority, an anonymous Group must have 2 members only"
491 .into(),
492 ))
493 }
494 }
495 }
496 if self.version.is_some() {
497 vec.extend(self.version.as_ref().unwrap().validate())
498 }
499 if self.attachments.is_some() {
500 for att in self.attachments.as_ref().unwrap().iter() {
501 vec.extend(att.validate())
502 }
503 }
504
505 vec
506 }
507}
508
509impl FromStr for Statement {
510 type Err = DataError;
511
512 fn from_str(s: &str) -> Result<Self, Self::Err> {
513 let map: Map<String, Value> = serde_json::from_str(s)?;
514 Self::from_json_obj(map)
515 }
516}
517
518impl TryFrom<StatementType> for Statement {
519 type Error = DataError;
520
521 fn try_from(value: StatementType) -> Result<Self, Self::Error> {
522 match value {
523 StatementType::S(x) => Ok(*x),
524 StatementType::SId(x) => Ok(Statement::from(x)),
525 _ => Err(DataError::Validation(ValidationError::ConstraintViolation(
526 "Not a Statement".into(),
527 ))),
528 }
529 }
530}
531
532impl TryFrom<StatementType> for StatementId {
533 type Error = DataError;
534
535 fn try_from(value: StatementType) -> Result<Self, Self::Error> {
536 match value {
537 StatementType::S(x) => Ok(StatementId::from(x)),
538 StatementType::SId(x) => Ok(*x),
539 _ => Err(DataError::Validation(ValidationError::ConstraintViolation(
540 "Not a StatementId".into(),
541 ))),
542 }
543 }
544}
545
546#[derive(Debug, Default)]
548pub struct StatementBuilder {
549 _id: Option<Uuid>,
550 _actor: Option<Actor>,
551 _verb: Option<Verb>,
552 _object: Option<StatementObject>,
553 _result: Option<XResult>,
554 _context: Option<Context>,
555 _timestamp: Option<MyTimestamp>,
556 _stored: Option<DateTime<Utc>>,
557 _authority: Option<Actor>,
558 _version: Option<MyVersion>,
559 _attachments: Option<Vec<Attachment>>,
560}
561
562impl StatementBuilder {
563 pub fn id(self, val: &str) -> Result<Self, DataError> {
568 let val = val.trim();
569 if val.is_empty() {
570 emit_error!(DataError::Validation(ValidationError::Empty("id".into())))
571 } else {
572 let uuid = Uuid::parse_str(val)?;
573 self.id_as_uuid(uuid)
574 }
575 }
576
577 pub fn id_as_uuid(mut self, uuid: Uuid) -> Result<Self, DataError> {
582 if uuid.is_nil() || uuid.is_max() {
583 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
584 "'id' should not be all 0's or 1's".into()
585 )))
586 } else {
587 self._id = Some(uuid);
588 Ok(self)
589 }
590 }
591
592 pub fn actor(mut self, val: Actor) -> Result<Self, DataError> {
596 val.check_validity()?;
597 self._actor = Some(val);
598 Ok(self)
599 }
600
601 pub fn verb(mut self, val: Verb) -> Result<Self, DataError> {
605 val.check_validity()?;
606 self._verb = Some(val);
607 Ok(self)
608 }
609
610 pub fn object(mut self, val: StatementObject) -> Result<Self, DataError> {
614 val.check_validity()?;
615 self._object = Some(val);
616 Ok(self)
617 }
618
619 pub fn result(mut self, val: XResult) -> Result<Self, DataError> {
623 val.check_validity()?;
624 self._result = Some(val);
625 Ok(self)
626 }
627
628 pub fn context(mut self, val: Context) -> Result<Self, DataError> {
632 val.check_validity()?;
633 self._context = Some(val);
634 Ok(self)
635 }
636
637 pub fn timestamp(mut self, val: &str) -> Result<Self, DataError> {
641 let val = val.trim();
642 if val.is_empty() {
643 emit_error!(DataError::Validation(ValidationError::Empty(
644 "timestamp".into()
645 )))
646 }
647 let ts = MyTimestamp::from_str(val)?;
648 self._timestamp = Some(ts);
649 Ok(self)
650 }
651
652 pub fn with_timestamp(mut self, val: DateTime<Utc>) -> Self {
654 self._timestamp = Some(MyTimestamp::from(val));
655 self
656 }
657
658 pub fn stored(mut self, val: &str) -> Result<Self, DataError> {
662 let val = val.trim();
663 if val.is_empty() {
664 emit_error!(DataError::Validation(ValidationError::Empty(
665 "stored".into()
666 )))
667 }
668 let ts = serde_json::from_str::<MyTimestamp>(val)?;
669 self._stored = Some(*ts.inner());
670 Ok(self)
671 }
672
673 pub fn with_stored(mut self, val: DateTime<Utc>) -> Self {
675 self._stored = Some(val);
676 self
677 }
678
679 pub fn authority(mut self, val: Actor) -> Result<Self, DataError> {
683 val.check_validity()?;
684 if val.is_group() {
687 let group = val.as_group().unwrap();
688 if !group.is_anonymous() {
689 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
690 "When used as an Authority, a Group must be anonymous".into()
691 )))
692 }
693 if group.members().len() != 2 {
694 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
695 "When used as an Authority, an anonymous Group must have 2 members only".into()
696 )))
697 }
698 }
699 self._authority = Some(val);
700 Ok(self)
701 }
702
703 pub fn version(mut self, val: &str) -> Result<Self, DataError> {
707 self._version = Some(MyVersion::from_str(val)?);
708 Ok(self)
709 }
710
711 pub fn attachment(mut self, att: Attachment) -> Result<Self, DataError> {
714 att.check_validity()?;
715 if self._attachments.is_none() {
716 self._attachments = Some(vec![])
717 }
718 self._attachments.as_mut().unwrap().push(att);
719 Ok(self)
720 }
721
722 pub fn build(self) -> Result<Statement, DataError> {
726 if self._actor.is_none() || self._verb.is_none() || self._object.is_none() {
727 emit_error!(DataError::Validation(ValidationError::MissingField(
728 "actor, verb, or object".into()
729 )))
730 }
731 Ok(Statement {
732 id: self._id,
733 actor: self._actor.unwrap(),
734 verb: self._verb.unwrap(),
735 object: self._object.unwrap(),
736 result: self._result,
737 context: self._context,
738 timestamp: self._timestamp,
739 stored: self._stored,
740 authority: self._authority,
741 version: self._version,
742 attachments: self._attachments,
743 })
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750 use serde_json::{Map, Value};
751 use tracing_test::traced_test;
752
753 #[traced_test]
754 #[test]
755 #[should_panic]
756 fn test_extra_properties() {
757 const S: &str = r#"{
758"actor":{"objectType":"Agent","name":"xAPI mbox","mbox":"mailto:xapi@adlnet.gov"},
759"verb":{"id":"http://adlnet.gov/expapi/verbs/attended","display":{"en-US":"attended"}},
760"object":{"objectType":"Activity","id":"http://www.example.com/meetings/occurances/34534"},
761"iD":"46bf512f-56ec-45ef-8f95-1f4b352386e6"}"#;
762
763 let map: Map<String, Value> = serde_json::from_str(S).unwrap();
764 assert!(!map.contains_key("id"));
765 assert!(!map.contains_key("ID"));
766 assert!(!map.contains_key("Id"));
767 assert!(map.contains_key("iD"));
768 let s = serde_json::from_value::<Statement>(Value::Object(map));
769 assert!(s.is_err());
770
771 Statement::from_str(S).unwrap();
773 }
774
775 #[traced_test]
776 #[test]
777 fn test_extensions_w_nulls() {
778 const S: &str = r#"{
779"actor":{"objectType":"Agent","name":"xAPI account","mbox":"mailto:xapi@adlnet.gov"},
780"verb":{"id":"http://adlnet.gov/expapi/verbs/attended","display":{"en-GB":"attended"}},
781"object":{
782 "objectType":"Activity",
783 "id":"http://www.example.com/meetings/occurances/34534",
784 "definition":{
785 "type":"http://adlnet.gov/expapi/activities/meeting",
786 "name":{"en-GB":"example meeting","en-US":"example meeting"},
787 "description":{"en-GB":"An example meeting.","en-US":"An example meeting."},
788 "moreInfo":"http://virtualmeeting.example.com/345256",
789 "extensions":{"http://example.com/null":null}}}}"#;
790
791 assert!(Statement::from_str(S).is_ok());
792 }
793
794 #[test]
795 #[should_panic]
796 fn test_bad_duration() {
797 const S: &str = r#"{
798"actor":{"objectType":"Agent","name":"xAPI account","mbox":"mailto:xapi@adlnet.gov"},
799"verb":{"id":"http://adlnet.gov/expapi/verbs/attended","display":{"en-US":"attended"}},
800"result":{
801 "score":{"scaled":0.95,"raw":95,"min":0,"max":100},
802 "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"}},
803 "success":true,
804 "completion":true,
805 "response":"We agreed on some example actions.",
806 "duration":"P4W1D"},
807"object":{"objectType":"Activity","id":"http://www.example.com/meetings/occurances/34534"}}"#;
808
809 Statement::from_str(S).unwrap();
810 }
811}