1use crate::{
4 MyLanguageTag,
5 data::{
6 ActivityDefinition, Canonical, DataError, Extensions, Fingerprint, InteractionComponent,
7 InteractionType, ObjectType, Validate, ValidationError,
8 },
9 emit_error,
10};
11use core::fmt;
12use iri_string::types::{IriStr, IriString};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use serde_with::skip_serializing_none;
16use std::{
17 hash::{Hash, Hasher},
18 mem,
19 str::FromStr,
20};
21
22#[skip_serializing_none]
35#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
36pub struct Activity {
37 #[serde(rename = "objectType")]
38 object_type: Option<ObjectType>,
39 id: IriString,
40 definition: Option<ActivityDefinition>,
41}
42
43#[derive(Debug, Serialize)]
44pub(crate) struct ActivityId {
45 id: IriString,
46}
47
48impl From<Activity> for ActivityId {
49 fn from(value: Activity) -> Self {
50 ActivityId { id: value.id }
51 }
52}
53
54impl From<ActivityId> for Activity {
55 fn from(value: ActivityId) -> Self {
56 Activity {
57 object_type: None,
58 id: value.id,
59 definition: None,
60 }
61 }
62}
63
64impl Activity {
65 pub fn from_iri_str(iri: &str) -> Result<Self, DataError> {
68 Activity::builder().id(iri)?.build()
69 }
70
71 pub fn builder() -> ActivityBuilder<'static> {
73 ActivityBuilder::default()
74 }
75
76 pub fn id(&self) -> &IriStr {
78 &self.id
79 }
80
81 pub fn id_as_str(&self) -> &str {
83 self.id.as_str()
84 }
85
86 pub fn definition(&self) -> Option<&ActivityDefinition> {
88 self.definition.as_ref()
89 }
90
91 pub fn merge(&mut self, other: Activity) {
93 if self.id == other.id {
97 if self.definition.is_none() {
98 if other.definition.is_some() {
99 let x = mem::take(&mut other.definition.unwrap());
100 let mut z = Some(x);
101 mem::swap(&mut self.definition, &mut z);
102 }
103 } else if other.definition.is_some() {
104 let mut x = mem::take(&mut self.definition).unwrap();
105 let y = other.definition.unwrap();
106 x.merge(y);
107 let mut z = Some(x);
108 mem::swap(&mut self.definition, &mut z);
109 }
110 }
111 }
112
113 pub fn name(&self, tag: &MyLanguageTag) -> Option<&str> {
118 match &self.definition {
119 None => None,
120 Some(def) => def.name(tag),
121 }
122 }
123
124 pub fn description(&self, tag: &MyLanguageTag) -> Option<&str> {
128 match &self.definition {
129 None => None,
130 Some(def) => def.description(tag),
131 }
132 }
133
134 pub fn type_(&self) -> Option<&IriStr> {
137 match &self.definition {
138 None => None,
139 Some(def) => def.type_(),
140 }
141 }
142
143 pub fn more_info(&self) -> Option<&IriStr> {
149 match &self.definition {
150 None => None,
151 Some(def) => def.more_info(),
152 }
153 }
154
155 pub fn interaction_type(&self) -> Option<&InteractionType> {
169 match &self.definition {
170 None => None,
171 Some(def) => def.interaction_type(),
172 }
173 }
174
175 pub fn correct_responses_pattern(&self) -> Option<&Vec<String>> {
183 match &self.definition {
184 None => None,
185 Some(def) => def.correct_responses_pattern(),
186 }
187 }
188
189 pub fn choices(&self) -> Option<&Vec<InteractionComponent>> {
198 match &self.definition {
199 None => None,
200 Some(def) => def.choices(),
201 }
202 }
203
204 pub fn scale(&self) -> Option<&Vec<InteractionComponent>> {
213 match &self.definition {
214 None => None,
215 Some(def) => def.scale(),
216 }
217 }
218
219 pub fn source(&self) -> Option<&Vec<InteractionComponent>> {
228 match &self.definition {
229 None => None,
230 Some(def) => def.source(),
231 }
232 }
233
234 pub fn target(&self) -> Option<&Vec<InteractionComponent>> {
243 match &self.definition {
244 None => None,
245 Some(def) => def.target(),
246 }
247 }
248
249 pub fn steps(&self) -> Option<&Vec<InteractionComponent>> {
258 match &self.definition {
259 None => None,
260 Some(def) => def.steps(),
261 }
262 }
263
264 pub fn extensions(&self) -> Option<&Extensions> {
267 match &self.definition {
268 None => None,
269 Some(def) => def.extensions(),
270 }
271 }
272
273 pub fn extension(&self, key: &IriStr) -> Option<&Value> {
276 match &self.definition {
277 None => None,
278 Some(def) => def.extension(key),
279 }
280 }
281
282 pub fn set_object_type(&mut self) {
284 self.object_type = Some(ObjectType::Activity);
285 }
286}
287
288impl fmt::Display for Activity {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 let mut vec = vec![];
291 vec.push(format!("id: \"{}\"", self.id));
292 if self.definition.is_some() {
293 vec.push(format!("definition: {}", self.definition.as_ref().unwrap()))
294 }
295 let res = vec
296 .iter()
297 .map(|x| x.to_string())
298 .collect::<Vec<_>>()
299 .join(", ");
300 write!(f, "Activity{{ {res} }}")
301 }
302}
303
304impl Fingerprint for Activity {
305 fn fingerprint<H: Hasher>(&self, state: &mut H) {
306 let (x, y) = self.id.as_slice().to_absolute_and_fragment();
308 x.normalize().to_string().hash(state);
309 y.hash(state);
310 }
312}
313
314impl Validate for Activity {
315 fn validate(&self) -> Vec<ValidationError> {
316 let mut vec = vec![];
317
318 if self.object_type.is_some() && *self.object_type.as_ref().unwrap() != ObjectType::Activity
319 {
320 vec.push(ValidationError::WrongObjectType {
321 expected: ObjectType::Activity,
322 found: self.object_type.as_ref().unwrap().to_string().into(),
323 })
324 }
325 if self.id.is_empty() {
326 vec.push(ValidationError::Empty("id".into()))
327 }
328 if self.definition.is_some() {
329 vec.extend(self.definition.as_ref().unwrap().validate());
330 }
331
332 vec
333 }
334}
335
336impl Canonical for Activity {
337 fn canonicalize(&mut self, language_tags: &[MyLanguageTag]) {
338 if self.definition.is_some() {
339 self.definition
340 .as_mut()
341 .unwrap()
342 .canonicalize(language_tags);
343 }
344 }
345}
346
347impl FromStr for Activity {
348 type Err = DataError;
349
350 fn from_str(s: &str) -> Result<Self, Self::Err> {
351 let x = serde_json::from_str::<Activity>(s)?;
352 x.check_validity()?;
353 Ok(x)
354 }
355}
356
357#[derive(Debug, Default)]
359pub struct ActivityBuilder<'a> {
360 _object_type: Option<ObjectType>,
361 _id: Option<&'a IriStr>,
362 _definition: Option<ActivityDefinition>,
363}
364
365impl<'a> ActivityBuilder<'a> {
366 pub fn with_object_type(mut self) -> Self {
368 self._object_type = Some(ObjectType::Activity);
369 self
370 }
371
372 pub fn id(mut self, val: &'a str) -> Result<Self, DataError> {
376 let id = val.trim();
377 if id.is_empty() {
378 emit_error!(DataError::Validation(ValidationError::Empty("id".into())))
379 } else {
380 let iri = IriStr::new(id)?;
381 assert!(
382 !iri.is_empty(),
383 "Activity identifier IRI should not be empty"
384 );
385 self._id = Some(iri);
386 Ok(self)
387 }
388 }
389
390 pub fn definition(mut self, val: ActivityDefinition) -> Result<Self, DataError> {
394 val.check_validity()?;
395 self._definition = Some(val);
396 Ok(self)
397 }
398
399 pub fn add_definition(mut self, val: ActivityDefinition) -> Result<Self, DataError> {
401 val.check_validity()?;
402 if self._definition.is_none() {
403 self._definition = Some(val)
404 } else {
405 let mut x = mem::take(&mut self._definition).unwrap();
406 x.merge(val);
407 let mut z = Some(x);
408 mem::swap(&mut self._definition, &mut z);
409 }
410 Ok(self)
411 }
412
413 pub fn build(self) -> Result<Activity, DataError> {
417 if self._id.is_none() {
418 emit_error!(DataError::Validation(ValidationError::MissingField(
419 "id".into()
420 )))
421 } else {
422 Ok(Activity {
423 object_type: self._object_type,
424 id: self._id.unwrap().to_owned(),
425 definition: self._definition,
426 })
427 }
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use std::collections::HashMap;
435 use tracing_test::traced_test;
436
437 #[traced_test]
438 #[test]
439 fn test_long_activity() {
440 const ROOM_KEY: &str =
441 "http://example.com/profiles/meetings/activitydefinitionextensions/room";
442 const JSON: &str = r#"{
443 "id": "http://www.example.com/meetings/occurances/34534",
444 "definition": {
445 "extensions": {
446 "http://example.com/profiles/meetings/activitydefinitionextensions/room": {
447 "name": "Kilby",
448 "id": "http://example.com/rooms/342"
449 }
450 },
451 "name": {
452 "en-GB": "example meeting",
453 "en-US": "example meeting"
454 },
455 "description": {
456 "en-GB": "An example meeting that happened on a specific occasion with certain people present.",
457 "en-US": "An example meeting that happened on a specific occasion with certain people present."
458 },
459 "type": "http://adlnet.gov/expapi/activities/meeting",
460 "moreInfo": "http://virtualmeeting.example.com/345256"
461 },
462 "objectType": "Activity"
463 }"#;
464
465 let room_iri = IriStr::new(ROOM_KEY).expect("Failed parsing IRI");
466 let de_result = serde_json::from_str::<Activity>(JSON);
467 assert!(de_result.is_ok());
468 let activity = de_result.unwrap();
469
470 let definition = activity.definition().unwrap();
471 assert!(definition.more_info().is_some());
472 assert_eq!(
473 definition.more_info().unwrap(),
474 "http://virtualmeeting.example.com/345256"
475 );
476
477 assert!(definition.extensions().is_some());
478 let ext = definition.extensions().unwrap();
479 assert!(ext.contains_key(room_iri));
480
481 let room_info = ext.get(room_iri).unwrap();
483 let room = serde_json::from_value::<HashMap<String, String>>(room_info.clone()).unwrap();
484 assert!(room.contains_key("name"));
485 assert_eq!(room.get("name"), Some(&String::from("Kilby")));
486 assert!(room.contains_key("id"));
487 assert_eq!(
488 room.get("id"),
489 Some(&String::from("http://example.com/rooms/342"))
490 );
491 }
492
493 #[traced_test]
494 #[test]
495 fn test_merge() -> Result<(), DataError> {
496 const XT_LOCATION: &str = "http://example.com/xt/meeting/location";
497 const XT_REPORTER: &str = "http://example.com/xt/meeting/reporter";
498 const MORE_INFO: &str = "http://virtualmeeting.example.com/345256";
499 const V1: &str = r#"{
500 "id": "http://www.example.com/test",
501 "definition": {
502 "name": {
503 "en-GB": "attended",
504 "en-US": "attended"
505 },
506 "description": {
507 "en-US": "On this map, please mark Franklin, TN"
508 },
509 "type": "http://adlnet.gov/expapi/activities/cmi.interaction",
510 "moreInfo": "http://virtualmeeting.example.com/345256",
511 "interactionType": "other"
512 }
513 }"#;
514 const V2: &str = r#"{
515 "objectType": "Activity",
516 "id": "http://www.example.com/test",
517 "definition": {
518 "name": {
519 "en": "Other",
520 "ja-JP": "出席した",
521 "ko-KR": "참석",
522 "is-IS": "sótti",
523 "ru-RU": "участие",
524 "pa-IN": "ਹਾਜ਼ਰ",
525 "sk-SK": "zúčastnil",
526 "ar-EG": "حضر"
527 },
528 "extensions": {
529 "http://example.com/xt/meeting/location": "X:\\meetings\\minutes\\examplemeeting.one"
530 }
531 }
532 }"#;
533 const V3: &str = r#"{
534 "id": "http://www.example.com/test",
535 "definition": {
536 "correctResponsesPattern": [ "(35.937432,-86.868896)" ],
537 "extensions": {
538 "http://example.com/xt/meeting/reporter": {
539 "name": "Thomas",
540 "id": "http://openid.com/342"
541 }
542 }
543 }
544 }"#;
545
546 let location_iri = IriStr::new(XT_LOCATION).expect("Failed parsing XT_LOCATION IRI");
547 let reporter_iri = IriStr::new(XT_REPORTER).expect("Failed parsing XT_REPORTER IRI");
548
549 let en = MyLanguageTag::from_str("en-GB")?;
550 let ko = MyLanguageTag::from_str("ko-KR")?;
551
552 let mut v1 = serde_json::from_str::<Activity>(V1).unwrap();
553 let v2 = serde_json::from_str::<Activity>(V2).unwrap();
554 let v3 = serde_json::from_str::<Activity>(V3).unwrap();
555
556 v1.merge(v2);
557
558 assert_eq!(
560 v1.definition()
561 .unwrap()
562 .more_info()
563 .expect("Failed finding `more_info` after merging V2"),
564 MORE_INFO
565 );
566 assert_eq!(v1.definition().unwrap().name(&en).unwrap(), "attended");
567
568 assert_eq!(v1.definition().unwrap().name(&ko).unwrap(), "참석");
570 assert_eq!(v1.definition().unwrap().extensions().unwrap().len(), 1);
572 assert!(
573 v1.definition()
574 .unwrap()
575 .extensions()
576 .unwrap()
577 .contains_key(location_iri)
578 );
579
580 v1.merge(v3);
581
582 assert_eq!(v1.definition().unwrap().extensions().unwrap().len(), 2);
583 assert!(
584 v1.definition()
585 .unwrap()
586 .extensions()
587 .unwrap()
588 .contains_key(reporter_iri)
589 );
590
591 Ok(())
592 }
593
594 #[test]
595 fn test_validity() {
596 const BAD: &str = r#"{"objectType":"Activity","id":"http://www.example.com/meetings/categories/teammeeting","definition":{"name":{"en":"Fill-In"},"description":{"en":"Ben is often heard saying:"},"type":"http://adlnet.gov/expapi/activities/cmi.interaction","moreInfo":"http://virtualmeeting.example.com/345256","correctResponsesPattern":["Bob's your uncle"],"extensions":{"http://example.com/profiles/meetings/extension/location":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one","http://example.com/profiles/meetings/extension/reporter":{"name":"Thomas","id":"http://openid.com/342"}}}}"#;
597
598 let res = serde_json::from_str::<Activity>(BAD);
600 assert!(res.is_ok());
601 let act = res.unwrap();
603 assert!(!act.is_valid());
604
605 let res = Activity::from_str(BAD);
607 assert!(res.is_err());
608 }
609
610 #[test]
611 fn test_merge_definition() -> Result<(), DataError> {
612 const A1: &str = r#"{
613"objectType":"Activity",
614"id":"http://www.xapi.net/activity/12345",
615"definition":{
616 "type":"http://adlnet.gov/expapi/activities/meeting",
617 "name":{"en-GB":"meeting","en-US":"meeting"},
618 "description":{"en-US":"A past meeting."},
619 "moreInfo":"https://xapi.net/more/345256",
620 "extensions":{
621 "http://example.com/profiles/meetings/extension/location":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one",
622 "http://example.com/profiles/meetings/extension/reporter":{"name":"Larry","id":"http://openid.com/342"}
623 }
624}}"#;
625 const A2: &str = r#"{
626"objectType":"Activity",
627"id":"http://www.xapi.net/activity/12345",
628"definition":{
629 "type":"http://adlnet.gov/expapi/activities/meeting",
630 "name":{"en-GB":"meeting","fr-FR":"réunion"},
631 "description":{"en-GB":"A past meeting."},
632 "moreInfo":"https://xapi.net/more/345256",
633 "extensions":{
634 "http://example.com/profiles/meetings/extension/location":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one",
635 "http://example.com/profiles/meetings/extension/editor":{"name":"Curly","id":"http://openid.com/342"}
636 }
637}}"#;
638 let en = MyLanguageTag::from_str("en-GB")?;
639 let am = MyLanguageTag::from_str("en-US")?;
640 let fr = MyLanguageTag::from_str("fr-FR")?;
641
642 let mut a1 = Activity::from_str(A1).unwrap();
643 assert_eq!(a1.name(&en), Some("meeting"));
644 assert_eq!(a1.name(&am), Some("meeting"));
645 assert!(a1.name(&fr).is_none());
646 assert_eq!(a1.description(&am), Some("A past meeting."));
647 assert!(a1.description(&en).is_none());
648 assert_eq!(a1.extensions().map_or(0, |x| x.len()), 2);
649
650 let a2 = Activity::from_str(A2).unwrap();
651 assert_eq!(a2.name(&en), Some("meeting"));
652 assert_eq!(a2.name(&fr), Some("réunion"));
653 assert!(a2.name(&am).is_none());
654 assert_eq!(a2.description(&en), Some("A past meeting."));
655 assert!(a2.description(&am).is_none());
656 assert_eq!(a2.extensions().map_or(0, |x| x.len()), 2);
657
658 a1.merge(a2);
659 assert_eq!(a1.name(&en), Some("meeting"));
660 assert_eq!(a1.name(&am), Some("meeting"));
661 assert_eq!(a1.name(&fr), Some("réunion"));
662 assert_eq!(a1.description(&am), Some("A past meeting."));
663 assert_eq!(a1.description(&en), Some("A past meeting."));
664 assert_eq!(a1.extensions().map_or(0, |x| x.len()), 3);
665
666 Ok(())
667 }
668}