1use crate::{
4 MyLanguageTag, add_language,
5 data::{
6 Canonical, DataError, Extensions, InteractionComponent, InteractionType, LanguageMap,
7 Validate, ValidationError, validate::validate_irl,
8 },
9 emit_error, merge_maps,
10};
11use core::fmt;
12use iri_string::types::{IriStr, IriString};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use serde_with::skip_serializing_none;
16
17#[skip_serializing_none]
20#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct ActivityDefinition {
23 name: Option<LanguageMap>,
24 description: Option<LanguageMap>,
25 #[serde(rename = "type")]
26 type_: Option<IriString>,
27 more_info: Option<IriString>,
28 interaction_type: Option<InteractionType>,
32 correct_responses_pattern: Option<Vec<String>>,
33 choices: Option<Vec<InteractionComponent>>,
34 scale: Option<Vec<InteractionComponent>>,
35 source: Option<Vec<InteractionComponent>>,
36 target: Option<Vec<InteractionComponent>>,
37 steps: Option<Vec<InteractionComponent>>,
38 extensions: Option<Extensions>,
39}
40
41impl ActivityDefinition {
42 pub fn builder() -> ActivityDefinitionBuilder<'static> {
44 ActivityDefinitionBuilder::default()
45 }
46
47 pub fn name(&self, tag: &MyLanguageTag) -> Option<&str> {
50 match &self.name {
51 Some(lm) => lm.get(tag),
52 None => None,
53 }
54 }
55
56 pub fn description(&self, tag: &MyLanguageTag) -> Option<&str> {
59 match &self.description {
60 Some(lm) => lm.get(tag),
61 None => None,
62 }
63 }
64
65 pub fn type_(&self) -> Option<&IriStr> {
67 self.type_.as_deref()
68 }
69
70 pub fn more_info(&self) -> Option<&IriStr> {
75 self.more_info.as_deref()
76 }
77
78 pub fn interaction_type(&self) -> Option<&InteractionType> {
91 self.interaction_type.as_ref()
92 }
93
94 pub fn correct_responses_pattern(&self) -> Option<&Vec<String>> {
101 self.correct_responses_pattern.as_ref()
102 }
103
104 pub fn choices(&self) -> Option<&Vec<InteractionComponent>> {
112 self.choices.as_ref()
113 }
114
115 pub fn scale(&self) -> Option<&Vec<InteractionComponent>> {
123 self.scale.as_ref()
124 }
125
126 pub fn source(&self) -> Option<&Vec<InteractionComponent>> {
134 self.source.as_ref()
135 }
136
137 pub fn target(&self) -> Option<&Vec<InteractionComponent>> {
145 self.target.as_ref()
146 }
147
148 pub fn steps(&self) -> Option<&Vec<InteractionComponent>> {
156 self.steps.as_ref()
157 }
158
159 pub fn extensions(&self) -> Option<&Extensions> {
161 self.extensions.as_ref()
162 }
163
164 pub fn extension(&self, key: &IriStr) -> Option<&Value> {
166 if self.extensions.is_none() {
167 None
168 } else {
169 self.extensions.as_ref().unwrap().get(key)
170 }
171 }
172
173 pub fn merge(&mut self, that: Self) {
175 fn merge_opt_collections(
177 dst: &mut Option<Vec<InteractionComponent>>,
178 src: Option<Vec<InteractionComponent>>,
179 ) {
180 match dst {
181 Some(lhs) => {
182 if let Some(rhs) = src {
183 InteractionComponent::merge_collections(lhs, rhs)
184 }
185 }
186 None => *dst = src,
187 }
188 }
189
190 merge_maps!(&mut self.name, that.name);
192 merge_maps!(&mut self.description, that.description);
193 merge_maps!(&mut self.extensions, that.extensions);
194 if self.type_.is_none() {
196 self.type_ = that.type_
197 }
198 if self.more_info.is_none() {
199 self.more_info = that.more_info
200 }
201 if self.interaction_type.is_none() {
202 self.interaction_type = that.interaction_type
203 }
204 match &mut self.correct_responses_pattern {
206 Some(this_field) => {
207 if let Some(that_field) = that.correct_responses_pattern {
208 this_field.extend(that_field);
209 this_field.sort();
211 this_field.dedup();
212 }
213 }
214 None => self.correct_responses_pattern = that.correct_responses_pattern,
215 }
216 merge_opt_collections(&mut self.choices, that.choices);
218 merge_opt_collections(&mut self.scale, that.scale);
219 merge_opt_collections(&mut self.source, that.source);
220 merge_opt_collections(&mut self.target, that.target);
221 merge_opt_collections(&mut self.steps, that.steps);
222 }
223}
224
225impl fmt::Display for ActivityDefinition {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 let mut vec = vec![];
228 if self.name.is_some() {
229 vec.push(format!("name: {}", self.name.as_ref().unwrap()));
230 }
231 if self.description.is_some() {
232 vec.push(format!(
233 "description: {}",
234 self.description.as_ref().unwrap()
235 ));
236 }
237 if self.type_.is_some() {
238 vec.push(format!("type: \"{}\"", self.type_.as_ref().unwrap()));
239 }
240 if self.more_info.is_some() {
241 vec.push(format!(
242 "moreInfo: \"{}\"",
243 self.more_info.as_ref().unwrap()
244 ));
245 }
246 if self.interaction_type.is_some() {
247 vec.push(format!(
248 "interactionType: {}",
249 self.interaction_type.as_ref().unwrap()
250 ));
251 }
252 if self.correct_responses_pattern.is_some() {
253 vec.push(format!(
254 "correctResponsesPattern: {}",
255 array_to_display_str(self.correct_responses_pattern.as_ref().unwrap())
256 ));
257 }
258 if self.choices.is_some() {
259 vec.push(format!(
260 "choices: {}",
261 vec_to_display_str(self.choices.as_ref().unwrap())
262 ));
263 }
264 if self.scale.is_some() {
265 vec.push(format!(
266 "scale: {}",
267 vec_to_display_str(self.scale.as_ref().unwrap())
268 ));
269 }
270 if self.source.is_some() {
271 vec.push(format!(
272 "source: {}",
273 vec_to_display_str(self.source.as_ref().unwrap())
274 ));
275 }
276 if self.target.is_some() {
277 vec.push(format!(
278 "target: {}",
279 vec_to_display_str(self.target.as_ref().unwrap())
280 ));
281 }
282 if self.steps.is_some() {
283 vec.push(format!(
284 "steps: {}",
285 vec_to_display_str(self.steps.as_ref().unwrap())
286 ));
287 }
288 if self.extensions.is_some() {
289 vec.push(format!("extensions: {}", self.extensions.as_ref().unwrap()))
290 }
291 let res = vec
292 .iter()
293 .map(|x| x.to_string())
294 .collect::<Vec<_>>()
295 .join(", ");
296 write!(f, "ActivityDefinition{{ {res} }}")
297 }
298}
299
300impl Validate for ActivityDefinition {
301 fn validate(&self) -> Vec<ValidationError> {
302 let mut vec: Vec<ValidationError> = vec![];
303
304 if self.type_.is_some() && self.type_.as_ref().unwrap().is_empty() {
306 vec.push(ValidationError::InvalidIRI("type".into()))
307 }
308 if self.more_info.is_some() {
310 validate_irl(self.more_info.as_ref().unwrap()).unwrap_or_else(|x| vec.push(x));
311 }
312 if (self.correct_responses_pattern.is_some()
314 || self.choices.is_some()
315 || self.scale.is_some()
316 || self.source.is_some()
317 || self.target.is_some()
318 || self.steps.is_some())
319 && self.interaction_type.is_none()
320 {
321 vec.push(ValidationError::ConstraintViolation(
322 "Activity definition interaction-type must be present when any Interaction Activities properties is too".into(),
323 ))
324 }
325 if self.correct_responses_pattern.is_some() {
327 for it in self.correct_responses_pattern.as_ref().unwrap().iter() {
328 if it.is_empty() {
329 vec.push(ValidationError::Empty("correctResponsePattern".into()))
330 }
331 }
332 }
333 if self.choices.is_some() {
335 self.choices
336 .as_ref()
337 .unwrap()
338 .iter()
339 .for_each(|x| vec.extend(x.validate()));
340 }
341 if self.scale.is_some() {
343 self.scale
344 .as_ref()
345 .unwrap()
346 .iter()
347 .for_each(|x| vec.extend(x.validate()));
348 }
349 if self.source.is_some() {
351 self.source
352 .as_ref()
353 .unwrap()
354 .iter()
355 .for_each(|x| vec.extend(x.validate()));
356 }
357 if self.target.is_some() {
359 self.target
360 .as_ref()
361 .unwrap()
362 .iter()
363 .for_each(|x| vec.extend(x.validate()));
364 }
365 if self.steps.is_some() {
367 self.steps
368 .as_ref()
369 .unwrap()
370 .iter()
371 .for_each(|x| vec.extend(x.validate()));
372 }
373
374 vec
375 }
376}
377
378impl Canonical for ActivityDefinition {
379 fn canonicalize(&mut self, language_tags: &[MyLanguageTag]) {
380 if self.name.is_some() {
381 self.name.as_mut().unwrap().canonicalize(language_tags)
382 }
383 if self.description.is_some() {
384 self.description
385 .as_mut()
386 .unwrap()
387 .canonicalize(language_tags)
388 }
389 if self.choices.is_some() {
390 for it in self.choices.as_mut().unwrap() {
391 it.canonicalize(language_tags)
392 }
393 }
394 if self.scale.is_some() {
395 for it in self.scale.as_mut().unwrap() {
396 it.canonicalize(language_tags)
397 }
398 }
399 if self.source.is_some() {
400 for it in self.source.as_mut().unwrap() {
401 it.canonicalize(language_tags)
402 }
403 }
404 if self.target.is_some() {
405 for it in self.target.as_mut().unwrap() {
406 it.canonicalize(language_tags)
407 }
408 }
409 if self.steps.is_some() {
410 for it in self.steps.as_mut().unwrap() {
411 it.canonicalize(language_tags)
412 }
413 }
414 }
415}
416
417#[derive(Debug, Default)]
419pub struct ActivityDefinitionBuilder<'a> {
420 _name: Option<LanguageMap>,
421 _description: Option<LanguageMap>,
422 _type_: Option<&'a IriStr>,
423 _more_info: Option<&'a IriStr>,
424 _interaction_type: Option<InteractionType>,
425 _correct_responses_pattern: Option<Vec<String>>,
426 _choices: Option<Vec<InteractionComponent>>,
427 _scale: Option<Vec<InteractionComponent>>,
428 _source: Option<Vec<InteractionComponent>>,
429 _target: Option<Vec<InteractionComponent>>,
430 _steps: Option<Vec<InteractionComponent>>,
431 _extensions: Option<Extensions>,
432}
433
434impl<'a> ActivityDefinitionBuilder<'a> {
435 pub fn name(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
439 add_language!(self._name, tag, label);
440 Ok(self)
441 }
442
443 pub fn description(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
448 add_language!(self._description, tag, label);
449 Ok(self)
450 }
451
452 pub fn type_(mut self, val: &'a str) -> Result<Self, DataError> {
454 let val = val.trim();
455 if val.is_empty() {
456 emit_error!(DataError::Validation(ValidationError::Empty("type".into())))
457 } else {
458 let iri = IriStr::new(val)?;
459 self._type_ = Some(iri);
460 Ok(self)
461 }
462 }
463
464 pub fn more_info(mut self, val: &'a str) -> Result<Self, DataError> {
466 let val = val.trim();
467 if val.is_empty() {
468 emit_error!(DataError::Validation(ValidationError::Empty(
469 "more_info".into()
470 )))
471 } else {
472 let val = IriStr::new(val)?;
473 validate_irl(val)?;
474 self._more_info = Some(val);
475 Ok(self)
476 }
477 }
478
479 pub fn interaction_type(mut self, val: InteractionType) -> Self {
481 self._interaction_type = Some(val);
482 self
483 }
484
485 pub fn correct_responses_pattern(mut self, val: &str) -> Result<Self, DataError> {
487 let val = val.trim();
488 if val.is_empty() {
489 emit_error!(DataError::Validation(ValidationError::Empty(
490 "correct_responses_pattern".into()
491 )))
492 }
493 if self._correct_responses_pattern.is_none() {
494 self._correct_responses_pattern = Some(vec![])
495 }
496 self._correct_responses_pattern
497 .as_mut()
498 .unwrap()
499 .push(val.to_string());
500 Ok(self)
501 }
502
503 pub fn choices(mut self, val: InteractionComponent) -> Result<Self, DataError> {
505 val.check_validity()?;
506 if self._choices.is_none() {
507 self._choices = Some(vec![])
508 }
509 self._choices.as_mut().unwrap().push(val);
510 Ok(self)
511 }
512
513 pub fn scale(mut self, val: InteractionComponent) -> Result<Self, DataError> {
515 val.check_validity()?;
516 if self._scale.is_none() {
517 self._scale = Some(vec![])
518 }
519 self._scale.as_mut().unwrap().push(val);
520 Ok(self)
521 }
522
523 pub fn source(mut self, val: InteractionComponent) -> Result<Self, DataError> {
525 val.check_validity()?;
526 if self._source.is_none() {
527 self._source = Some(vec![])
528 }
529 self._source.as_mut().unwrap().push(val);
530 Ok(self)
531 }
532
533 pub fn target(mut self, val: InteractionComponent) -> Result<Self, DataError> {
535 val.check_validity()?;
536 if self._target.is_none() {
537 self._target = Some(vec![])
538 }
539 self._target.as_mut().unwrap().push(val);
540 Ok(self)
541 }
542
543 pub fn steps(mut self, val: InteractionComponent) -> Result<Self, DataError> {
545 val.check_validity()?;
546 if self._steps.is_none() {
547 self._steps = Some(vec![])
548 }
549 self._steps.as_mut().unwrap().push(val);
550 Ok(self)
551 }
552
553 pub fn extension(mut self, key: &str, value: &Value) -> Result<Self, DataError> {
555 if self._extensions.is_none() {
556 self._extensions = Some(Extensions::new());
557 }
558 let _ = self._extensions.as_mut().unwrap().add(key, value);
559 Ok(self)
560 }
561
562 pub fn build(self) -> Result<ActivityDefinition, DataError> {
566 if self._name.is_none()
567 && self._description.is_none()
568 && self._type_.is_none()
569 && self._more_info.is_none()
570 && self._interaction_type.is_none()
571 && self._correct_responses_pattern.is_none()
572 && self._choices.is_none()
573 && self._scale.is_none()
574 && self._source.is_none()
575 && self._target.is_none()
576 && self._steps.is_none()
577 && self._extensions.is_none()
578 {
579 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
580 "At least 1 field must be set".into()
581 )))
582 }
583
584 if self._interaction_type.is_none()
585 && (self._correct_responses_pattern.is_some()
586 || self._choices.is_some()
587 || self._scale.is_some()
588 || self._source.is_some()
589 || self._target.is_some()
590 || self._steps.is_some())
591 {
592 emit_error!(DataError::Validation(ValidationError::MissingField(
593 "interaction_type".into()
594 )))
595 }
596
597 Ok(ActivityDefinition {
598 name: self._name,
599 description: self._description,
600 type_: if self._type_.is_none() {
601 None
602 } else {
603 Some(self._type_.unwrap().into())
604 },
605 more_info: if self._more_info.is_none() {
606 None
607 } else {
608 Some(self._more_info.unwrap().into())
609 },
610 interaction_type: self._interaction_type,
611 correct_responses_pattern: self._correct_responses_pattern,
612 choices: self._choices,
613 scale: self._scale,
614 source: self._source,
615 target: self._target,
616 steps: self._steps,
617
618 extensions: self._extensions,
619 })
620 }
621}
622
623fn array_to_display_str(val: &[String]) -> String {
624 let mut vec = vec![];
625 for v in val.iter() {
626 vec.push(format!("\"{v}\""))
627 }
628 vec.iter()
629 .map(|x| x.to_string())
630 .collect::<Vec<_>>()
631 .join(", ")
632}
633
634fn vec_to_display_str(val: &Vec<InteractionComponent>) -> String {
635 let mut vec = vec![];
636 for ic in val {
637 vec.push(format!("{ic}"))
638 }
639 vec.iter()
640 .map(|x| x.to_string())
641 .collect::<Vec<_>>()
642 .join(", ")
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648 use tracing_test::traced_test;
649
650 #[traced_test]
651 #[test]
652 fn test_display() {
653 const DISPLAY: &str = r#"ActivityDefinition{ description: {"en-US":"Does the xAPI include the concept of statements?"}, type: "http://adlnet.gov/expapi/activities/cmi.interaction", interactionType: true-false, correctResponsesPattern: "true" }"#;
654
655 let json = r#"{
656 "description": {
657 "en-US": "Does the xAPI include the concept of statements?"
658 },
659 "type": "http://adlnet.gov/expapi/activities/cmi.interaction",
660 "interactionType": "true-false",
661 "correctResponsesPattern": [
662 "true"
663 ]
664 }"#;
665
666 let de_result = serde_json::from_str::<ActivityDefinition>(json);
667 assert!(de_result.is_ok());
668 let ad = de_result.unwrap();
669 let display = format!("{}", ad);
670 assert_eq!(display, DISPLAY);
671 }
672
673 #[traced_test]
674 #[test]
675 fn test_missing_interaction_type() {
676 const BAD: &str = r#"{
677"name":{"en": "Fill-In"},
678"description":{"en": "Ben is often heard saying:"},
679"type":"http://adlnet.gov/expapi/activities/cmi.interaction",
680"moreInfo":"http://virtualmeeting.example.com/345256",
681"correctResponsesPattern":["Bob's your uncle"],
682"extensions":{
683 "http://example.com/profiles/meetings/extension/location":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one",
684 "http://example.com/profiles/meetings/extension/reporter":{"name":"Thomas","id":"http://openid.com/342"}
685}}"#;
686
687 let de_result = serde_json::from_str::<ActivityDefinition>(BAD);
688 assert!(de_result.is_ok());
689 let ad = de_result.unwrap();
690 assert!(!ad.is_valid());
692 }
693}