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