1use 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#[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#[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 pub fn from_json_obj(map: Map<String, Value>) -> Result<Self, DataError> {
135 for (k, v) in &map {
136 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 let stmt: Statement = serde_json::from_value(Value::Object(map.to_owned()))?;
149 stmt.check_validity()?;
150 Ok(stmt)
151 }
152
153 pub fn builder() -> StatementBuilder {
155 StatementBuilder::default()
156 }
157
158 pub fn id(&self) -> Option<&Uuid> {
161 self.id.as_ref()
162 }
163
164 pub fn set_id(&mut self, id: Uuid) {
166 self.id = Some(id)
167 }
168
169 pub fn actor(&self) -> &Actor {
175 &self.actor
176 }
177
178 pub fn verb(&self) -> &Verb {
180 &self.verb
181 }
182
183 pub fn is_verb_voided(&self) -> bool {
185 self.verb.is_voided()
186 }
187
188 pub fn object(&self) -> &StatementObject {
194 &self.object
195 }
196
197 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 pub fn result(&self) -> Option<&XResult> {
217 self.result.as_ref()
218 }
219
220 pub fn context(&self) -> Option<&Context> {
222 self.context.as_ref()
223 }
224
225 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 pub fn timestamp_internal(&self) -> Option<&MyTimestamp> {
242 self.timestamp.as_ref()
243 }
244
245 pub fn stored(&self) -> Option<&DateTime<Utc>> {
248 self.stored.as_ref()
249 }
250
251 pub fn set_stored(&mut self, val: DateTime<Utc>) {
253 self.stored = Some(val);
254 }
255
256 pub fn authority(&self) -> Option<&Actor> {
263 self.authority.as_ref()
264 }
265
266 pub fn set_authority_unchecked(&mut self, actor: Actor) {
269 self.authority = Some(actor)
270 }
271
272 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 pub fn attachments(&self) -> &[Attachment] {
288 match &self.attachments {
289 Some(x) => x,
290 None => &[],
291 }
292 }
293
294 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 pub fn set_attachments(&mut self, attachments: Vec<Attachment>) {
306 self.attachments = Some(attachments)
307 }
308
309 pub fn print(&self) -> String {
311 serde_json::to_string_pretty(self).unwrap_or_else(|_| String::from("$Statement"))
312 }
313
314 pub fn uid(&self) -> u64 {
316 fingerprint_it(self)
317 }
318
319 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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 pub fn with_timestamp(mut self, val: DateTime<Utc>) -> Self {
653 self._timestamp = Some(MyTimestamp::from(val));
654 self
655 }
656
657 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 pub fn with_stored(mut self, val: DateTime<Utc>) -> Self {
674 self._stored = Some(val);
675 self
676 }
677
678 pub fn authority(mut self, val: Actor) -> Result<Self, DataError> {
682 val.check_validity()?;
683 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 pub fn version(mut self, val: &str) -> Result<Self, DataError> {
706 self._version = Some(MyVersion::from_str(val)?);
707 Ok(self)
708 }
709
710 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 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 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}