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 let Some(z_timestamp) = self.timestamp.as_ref() {
233 Some(z_timestamp.inner())
234 } else {
235 None
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 let Some(z_version) = self.version.as_ref() {
279 Some(z_version)
280 } else {
281 None
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 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 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 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 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#[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 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 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 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 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 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 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 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 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 pub fn with_timestamp(mut self, val: DateTime<Utc>) -> Self {
650 self._timestamp = Some(MyTimestamp::from(val));
651 self
652 }
653
654 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 pub fn with_stored(mut self, val: DateTime<Utc>) -> Self {
671 self._stored = Some(val);
672 self
673 }
674
675 pub fn authority(mut self, val: Actor) -> Result<Self, DataError> {
679 val.check_validity()?;
680 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 pub fn version(mut self, val: &str) -> Result<Self, DataError> {
703 self._version = Some(MyVersion::from_str(val)?);
704 Ok(self)
705 }
706
707 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 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 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}