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 let Some(mut z_other_definition) = other.definition {
99 let x = mem::take(&mut z_other_definition);
100 let mut z = Some(x);
101 mem::swap(&mut self.definition, &mut z);
102 }
103 } else if let Some(y) = other.definition {
104 let mut x = mem::take(&mut self.definition).unwrap();
105 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 let Some(z_definition) = self.definition.as_ref() {
293 vec.push(format!("definition: {}", z_definition))
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 if let Some(z_object_type) = self.object_type.as_ref()
318 && *z_object_type != ObjectType::Activity
319 {
320 vec.push(ValidationError::WrongObjectType {
321 expected: ObjectType::Activity,
322 found: z_object_type.to_string().into(),
323 })
324 }
325
326 if self.id.is_empty() {
327 vec.push(ValidationError::Empty("id".into()))
328 }
329 if let Some(z_definition) = self.definition.as_ref() {
330 vec.extend(z_definition.validate());
331 }
332
333 vec
334 }
335}
336
337impl Canonical for Activity {
338 fn canonicalize(&mut self, language_tags: &[MyLanguageTag]) {
339 if let Some(z_definition) = &mut self.definition {
340 z_definition.canonicalize(language_tags);
341 }
342 }
343}
344
345impl FromStr for Activity {
346 type Err = DataError;
347
348 fn from_str(s: &str) -> Result<Self, Self::Err> {
349 let x = serde_json::from_str::<Activity>(s)?;
350 x.check_validity()?;
351 Ok(x)
352 }
353}
354
355#[derive(Debug, Default)]
357pub struct ActivityBuilder<'a> {
358 _object_type: Option<ObjectType>,
359 _id: Option<&'a IriStr>,
360 _definition: Option<ActivityDefinition>,
361}
362
363impl<'a> ActivityBuilder<'a> {
364 pub fn with_object_type(mut self) -> Self {
366 self._object_type = Some(ObjectType::Activity);
367 self
368 }
369
370 pub fn id(mut self, val: &'a str) -> Result<Self, DataError> {
374 let id = val.trim();
375 if id.is_empty() {
376 emit_error!(DataError::Validation(ValidationError::Empty("id".into())))
377 } else {
378 let iri = IriStr::new(id)?;
379 assert!(
380 !iri.is_empty(),
381 "Activity identifier IRI should not be empty"
382 );
383 self._id = Some(iri);
384 Ok(self)
385 }
386 }
387
388 pub fn definition(mut self, val: ActivityDefinition) -> Result<Self, DataError> {
392 val.check_validity()?;
393 self._definition = Some(val);
394 Ok(self)
395 }
396
397 pub fn add_definition(mut self, val: ActivityDefinition) -> Result<Self, DataError> {
399 val.check_validity()?;
400 if self._definition.is_none() {
401 self._definition = Some(val)
402 } else {
403 let mut x = mem::take(&mut self._definition).unwrap();
404 x.merge(val);
405 let mut z = Some(x);
406 mem::swap(&mut self._definition, &mut z);
407 }
408 Ok(self)
409 }
410
411 pub fn build(self) -> Result<Activity, DataError> {
415 if let Some(z_id) = self._id {
416 Ok(Activity {
417 object_type: self._object_type,
418 id: z_id.to_owned(),
419 definition: self._definition,
420 })
421 } else {
422 emit_error!(DataError::Validation(ValidationError::MissingField(
423 "id".into()
424 )))
425 }
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use std::collections::HashMap;
433 use tracing_test::traced_test;
434
435 #[traced_test]
436 #[test]
437 fn test_long_activity() {
438 const ROOM_KEY: &str =
439 "http://example.com/profiles/meetings/activitydefinitionextensions/room";
440 const JSON: &str = r#"{
441 "id": "http://www.example.com/meetings/occurances/34534",
442 "definition": {
443 "extensions": {
444 "http://example.com/profiles/meetings/activitydefinitionextensions/room": {
445 "name": "Kilby",
446 "id": "http://example.com/rooms/342"
447 }
448 },
449 "name": {
450 "en-GB": "example meeting",
451 "en-US": "example meeting"
452 },
453 "description": {
454 "en-GB": "An example meeting that happened on a specific occasion with certain people present.",
455 "en-US": "An example meeting that happened on a specific occasion with certain people present."
456 },
457 "type": "http://adlnet.gov/expapi/activities/meeting",
458 "moreInfo": "http://virtualmeeting.example.com/345256"
459 },
460 "objectType": "Activity"
461 }"#;
462
463 let room_iri = IriStr::new(ROOM_KEY).expect("Failed parsing IRI");
464 let de_result = serde_json::from_str::<Activity>(JSON);
465 assert!(de_result.is_ok());
466 let activity = de_result.unwrap();
467
468 let definition = activity.definition().unwrap();
469 assert!(definition.more_info().is_some());
470 assert_eq!(
471 definition.more_info().unwrap(),
472 "http://virtualmeeting.example.com/345256"
473 );
474
475 assert!(definition.extensions().is_some());
476 let ext = definition.extensions().unwrap();
477 assert!(ext.contains_key(room_iri));
478
479 let room_info = ext.get(room_iri).unwrap();
481 let room = serde_json::from_value::<HashMap<String, String>>(room_info.clone()).unwrap();
482 assert!(room.contains_key("name"));
483 assert_eq!(room.get("name"), Some(&String::from("Kilby")));
484 assert!(room.contains_key("id"));
485 assert_eq!(
486 room.get("id"),
487 Some(&String::from("http://example.com/rooms/342"))
488 );
489 }
490
491 #[traced_test]
492 #[test]
493 fn test_merge() -> Result<(), DataError> {
494 const XT_LOCATION: &str = "http://example.com/xt/meeting/location";
495 const XT_REPORTER: &str = "http://example.com/xt/meeting/reporter";
496 const MORE_INFO: &str = "http://virtualmeeting.example.com/345256";
497 const V1: &str = r#"{
498 "id": "http://www.example.com/test",
499 "definition": {
500 "name": {
501 "en-GB": "attended",
502 "en-US": "attended"
503 },
504 "description": {
505 "en-US": "On this map, please mark Franklin, TN"
506 },
507 "type": "http://adlnet.gov/expapi/activities/cmi.interaction",
508 "moreInfo": "http://virtualmeeting.example.com/345256",
509 "interactionType": "other"
510 }
511 }"#;
512 const V2: &str = r#"{
513 "objectType": "Activity",
514 "id": "http://www.example.com/test",
515 "definition": {
516 "name": {
517 "en": "Other",
518 "ja-JP": "出席した",
519 "ko-KR": "참석",
520 "is-IS": "sótti",
521 "ru-RU": "участие",
522 "pa-IN": "ਹਾਜ਼ਰ",
523 "sk-SK": "zúčastnil",
524 "ar-EG": "حضر"
525 },
526 "extensions": {
527 "http://example.com/xt/meeting/location": "X:\\meetings\\minutes\\examplemeeting.one"
528 }
529 }
530 }"#;
531 const V3: &str = r#"{
532 "id": "http://www.example.com/test",
533 "definition": {
534 "correctResponsesPattern": [ "(35.937432,-86.868896)" ],
535 "extensions": {
536 "http://example.com/xt/meeting/reporter": {
537 "name": "Thomas",
538 "id": "http://openid.com/342"
539 }
540 }
541 }
542 }"#;
543
544 let location_iri = IriStr::new(XT_LOCATION).expect("Failed parsing XT_LOCATION IRI");
545 let reporter_iri = IriStr::new(XT_REPORTER).expect("Failed parsing XT_REPORTER IRI");
546
547 let en = MyLanguageTag::from_str("en-GB")?;
548 let ko = MyLanguageTag::from_str("ko-KR")?;
549
550 let mut v1 = serde_json::from_str::<Activity>(V1).unwrap();
551 let v2 = serde_json::from_str::<Activity>(V2).unwrap();
552 let v3 = serde_json::from_str::<Activity>(V3).unwrap();
553
554 v1.merge(v2);
555
556 assert_eq!(
558 v1.definition()
559 .unwrap()
560 .more_info()
561 .expect("Failed finding `more_info` after merging V2"),
562 MORE_INFO
563 );
564 assert_eq!(v1.definition().unwrap().name(&en).unwrap(), "attended");
565
566 assert_eq!(v1.definition().unwrap().name(&ko).unwrap(), "참석");
568 assert_eq!(v1.definition().unwrap().extensions().unwrap().len(), 1);
570 assert!(
571 v1.definition()
572 .unwrap()
573 .extensions()
574 .unwrap()
575 .contains_key(location_iri)
576 );
577
578 v1.merge(v3);
579
580 assert_eq!(v1.definition().unwrap().extensions().unwrap().len(), 2);
581 assert!(
582 v1.definition()
583 .unwrap()
584 .extensions()
585 .unwrap()
586 .contains_key(reporter_iri)
587 );
588
589 Ok(())
590 }
591
592 #[test]
593 fn test_validity() {
594 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"}}}}"#;
595
596 let res = serde_json::from_str::<Activity>(BAD);
598 assert!(res.is_ok());
599 let act = res.unwrap();
601 assert!(!act.is_valid());
602
603 let res = Activity::from_str(BAD);
605 assert!(res.is_err());
606 }
607
608 #[test]
609 fn test_merge_definition() -> Result<(), DataError> {
610 const A1: &str = r#"{
611"objectType":"Activity",
612"id":"http://www.xapi.net/activity/12345",
613"definition":{
614 "type":"http://adlnet.gov/expapi/activities/meeting",
615 "name":{"en-GB":"meeting","en-US":"meeting"},
616 "description":{"en-US":"A past meeting."},
617 "moreInfo":"https://xapi.net/more/345256",
618 "extensions":{
619 "http://example.com/profiles/meetings/extension/location":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one",
620 "http://example.com/profiles/meetings/extension/reporter":{"name":"Larry","id":"http://openid.com/342"}
621 }
622}}"#;
623 const A2: &str = r#"{
624"objectType":"Activity",
625"id":"http://www.xapi.net/activity/12345",
626"definition":{
627 "type":"http://adlnet.gov/expapi/activities/meeting",
628 "name":{"en-GB":"meeting","fr-FR":"réunion"},
629 "description":{"en-GB":"A past meeting."},
630 "moreInfo":"https://xapi.net/more/345256",
631 "extensions":{
632 "http://example.com/profiles/meetings/extension/location":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one",
633 "http://example.com/profiles/meetings/extension/editor":{"name":"Curly","id":"http://openid.com/342"}
634 }
635}}"#;
636 let en = MyLanguageTag::from_str("en-GB")?;
637 let am = MyLanguageTag::from_str("en-US")?;
638 let fr = MyLanguageTag::from_str("fr-FR")?;
639
640 let mut a1 = Activity::from_str(A1).unwrap();
641 assert_eq!(a1.name(&en), Some("meeting"));
642 assert_eq!(a1.name(&am), Some("meeting"));
643 assert!(a1.name(&fr).is_none());
644 assert_eq!(a1.description(&am), Some("A past meeting."));
645 assert!(a1.description(&en).is_none());
646 assert_eq!(a1.extensions().map_or(0, |x| x.len()), 2);
647
648 let a2 = Activity::from_str(A2).unwrap();
649 assert_eq!(a2.name(&en), Some("meeting"));
650 assert_eq!(a2.name(&fr), Some("réunion"));
651 assert!(a2.name(&am).is_none());
652 assert_eq!(a2.description(&en), Some("A past meeting."));
653 assert!(a2.description(&am).is_none());
654 assert_eq!(a2.extensions().map_or(0, |x| x.len()), 2);
655
656 a1.merge(a2);
657 assert_eq!(a1.name(&en), Some("meeting"));
658 assert_eq!(a1.name(&am), Some("meeting"));
659 assert_eq!(a1.name(&fr), Some("réunion"));
660 assert_eq!(a1.description(&am), Some("A past meeting."));
661 assert_eq!(a1.description(&en), Some("A past meeting."));
662 assert_eq!(a1.extensions().map_or(0, |x| x.len()), 3);
663
664 Ok(())
665 }
666}