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 let Some(z_extensions) = self.extensions.as_ref() {
167 z_extensions.get(key)
168 } else {
169 None
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 let Some(z_name) = self.name.as_ref() {
229 vec.push(format!("name: {}", z_name));
230 }
231 if let Some(z_description) = self.description.as_ref() {
232 vec.push(format!("description: {}", z_description));
233 }
234 if let Some(z_type) = self.type_.as_ref() {
235 vec.push(format!("type: \"{}\"", z_type));
236 }
237 if let Some(z_more_info) = self.more_info.as_ref() {
238 vec.push(format!("moreInfo: \"{}\"", z_more_info));
239 }
240 if let Some(z_interaction_type) = self.interaction_type.as_ref() {
241 vec.push(format!("interactionType: {}", z_interaction_type));
242 }
243 if let Some(z_correct_responses_pattern) = self.correct_responses_pattern.as_ref() {
244 vec.push(format!(
245 "correctResponsesPattern: {}",
246 array_to_display_str(z_correct_responses_pattern)
247 ));
248 }
249 if let Some(z_choices) = self.choices.as_ref() {
250 vec.push(format!("choices: {}", vec_to_display_str(z_choices)));
251 }
252 if let Some(z_scale) = self.scale.as_ref() {
253 vec.push(format!("scale: {}", vec_to_display_str(z_scale)));
254 }
255 if let Some(z_source) = self.source.as_ref() {
256 vec.push(format!("source: {}", vec_to_display_str(z_source)));
257 }
258 if let Some(z_target) = self.target.as_ref() {
259 vec.push(format!("target: {}", vec_to_display_str(z_target)));
260 }
261 if let Some(z_steps) = self.steps.as_ref() {
262 vec.push(format!("steps: {}", vec_to_display_str(z_steps)));
263 }
264 if let Some(z_extensions) = self.extensions.as_ref() {
265 vec.push(format!("extensions: {}", z_extensions))
266 }
267 let res = vec
268 .iter()
269 .map(|x| x.to_string())
270 .collect::<Vec<_>>()
271 .join(", ");
272 write!(f, "ActivityDefinition{{ {res} }}")
273 }
274}
275
276impl Validate for ActivityDefinition {
277 fn validate(&self) -> Vec<ValidationError> {
278 let mut vec: Vec<ValidationError> = vec![];
279
280 if self.type_.is_some() && self.type_.as_ref().unwrap().is_empty() {
282 vec.push(ValidationError::InvalidIRI("type".into()))
283 }
284 if let Some(z_more_info) = self.more_info.as_ref() {
286 validate_irl(z_more_info).unwrap_or_else(|x| vec.push(x));
287 }
288 if (self.correct_responses_pattern.is_some()
290 || self.choices.is_some()
291 || self.scale.is_some()
292 || self.source.is_some()
293 || self.target.is_some()
294 || self.steps.is_some())
295 && self.interaction_type.is_none()
296 {
297 vec.push(ValidationError::ConstraintViolation(
298 "Activity definition interaction-type must be present when any Interaction Activities properties is too".into(),
299 ))
300 }
301 if let Some(z_correct_responses_pattern) = self.correct_responses_pattern.as_ref() {
303 for it in z_correct_responses_pattern.iter() {
304 if it.is_empty() {
305 vec.push(ValidationError::Empty("correctResponsePattern".into()))
306 }
307 }
308 }
309 if let Some(z_choices) = self.choices.as_ref() {
311 z_choices.iter().for_each(|x| vec.extend(x.validate()));
312 }
313 if let Some(z_scale) = self.scale.as_ref() {
315 z_scale.iter().for_each(|x| vec.extend(x.validate()));
316 }
317 if let Some(z_source) = self.source.as_ref() {
319 z_source.iter().for_each(|x| vec.extend(x.validate()));
320 }
321 if let Some(z_target) = self.target.as_ref() {
323 z_target.iter().for_each(|x| vec.extend(x.validate()));
324 }
325 if let Some(z_steps) = self.steps.as_ref() {
327 z_steps.iter().for_each(|x| vec.extend(x.validate()));
328 }
329
330 vec
331 }
332}
333
334impl Canonical for ActivityDefinition {
335 fn canonicalize(&mut self, language_tags: &[MyLanguageTag]) {
336 if let Some(z_name) = self.name.as_mut() {
337 z_name.canonicalize(language_tags)
338 }
339 if let Some(z_description) = self.description.as_mut() {
340 z_description.canonicalize(language_tags)
341 }
342 if let Some(z_choices) = self.choices.as_mut() {
343 for it in z_choices {
344 it.canonicalize(language_tags)
345 }
346 }
347 if let Some(z_scale) = self.scale.as_mut() {
348 for it in z_scale {
349 it.canonicalize(language_tags)
350 }
351 }
352 if let Some(z_source) = self.source.as_mut() {
353 for it in z_source {
354 it.canonicalize(language_tags)
355 }
356 }
357 if let Some(z_target) = self.target.as_mut() {
358 for it in z_target {
359 it.canonicalize(language_tags)
360 }
361 }
362 if let Some(z_steps) = self.steps.as_mut() {
363 for it in z_steps {
364 it.canonicalize(language_tags)
365 }
366 }
367 }
368}
369
370#[derive(Debug, Default)]
372pub struct ActivityDefinitionBuilder<'a> {
373 _name: Option<LanguageMap>,
374 _description: Option<LanguageMap>,
375 _type_: Option<&'a IriStr>,
376 _more_info: Option<&'a IriStr>,
377 _interaction_type: Option<InteractionType>,
378 _correct_responses_pattern: Option<Vec<String>>,
379 _choices: Option<Vec<InteractionComponent>>,
380 _scale: Option<Vec<InteractionComponent>>,
381 _source: Option<Vec<InteractionComponent>>,
382 _target: Option<Vec<InteractionComponent>>,
383 _steps: Option<Vec<InteractionComponent>>,
384 _extensions: Option<Extensions>,
385}
386
387impl<'a> ActivityDefinitionBuilder<'a> {
388 pub fn name(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
392 add_language!(self._name, tag, label);
393 Ok(self)
394 }
395
396 pub fn description(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
401 add_language!(self._description, tag, label);
402 Ok(self)
403 }
404
405 pub fn type_(mut self, val: &'a str) -> Result<Self, DataError> {
407 let val = val.trim();
408 if val.is_empty() {
409 emit_error!(DataError::Validation(ValidationError::Empty("type".into())))
410 } else {
411 let iri = IriStr::new(val)?;
412 self._type_ = Some(iri);
413 Ok(self)
414 }
415 }
416
417 pub fn more_info(mut self, val: &'a str) -> Result<Self, DataError> {
419 let val = val.trim();
420 if val.is_empty() {
421 emit_error!(DataError::Validation(ValidationError::Empty(
422 "more_info".into()
423 )))
424 } else {
425 let val = IriStr::new(val)?;
426 validate_irl(val)?;
427 self._more_info = Some(val);
428 Ok(self)
429 }
430 }
431
432 pub fn interaction_type(mut self, val: InteractionType) -> Self {
434 self._interaction_type = Some(val);
435 self
436 }
437
438 pub fn correct_responses_pattern(mut self, val: &str) -> Result<Self, DataError> {
440 let val = val.trim();
441 if val.is_empty() {
442 emit_error!(DataError::Validation(ValidationError::Empty(
443 "correct_responses_pattern".into()
444 )))
445 }
446 if self._correct_responses_pattern.is_none() {
447 self._correct_responses_pattern = Some(vec![])
448 }
449 self._correct_responses_pattern
450 .as_mut()
451 .unwrap()
452 .push(val.to_string());
453 Ok(self)
454 }
455
456 pub fn choices(mut self, val: InteractionComponent) -> Result<Self, DataError> {
458 val.check_validity()?;
459 if self._choices.is_none() {
460 self._choices = Some(vec![])
461 }
462 self._choices.as_mut().unwrap().push(val);
463 Ok(self)
464 }
465
466 pub fn scale(mut self, val: InteractionComponent) -> Result<Self, DataError> {
468 val.check_validity()?;
469 if self._scale.is_none() {
470 self._scale = Some(vec![])
471 }
472 self._scale.as_mut().unwrap().push(val);
473 Ok(self)
474 }
475
476 pub fn source(mut self, val: InteractionComponent) -> Result<Self, DataError> {
478 val.check_validity()?;
479 if self._source.is_none() {
480 self._source = Some(vec![])
481 }
482 self._source.as_mut().unwrap().push(val);
483 Ok(self)
484 }
485
486 pub fn target(mut self, val: InteractionComponent) -> Result<Self, DataError> {
488 val.check_validity()?;
489 if self._target.is_none() {
490 self._target = Some(vec![])
491 }
492 self._target.as_mut().unwrap().push(val);
493 Ok(self)
494 }
495
496 pub fn steps(mut self, val: InteractionComponent) -> Result<Self, DataError> {
498 val.check_validity()?;
499 if self._steps.is_none() {
500 self._steps = Some(vec![])
501 }
502 self._steps.as_mut().unwrap().push(val);
503 Ok(self)
504 }
505
506 pub fn extension(mut self, key: &str, value: &Value) -> Result<Self, DataError> {
508 if self._extensions.is_none() {
509 self._extensions = Some(Extensions::new());
510 }
511 let _ = self._extensions.as_mut().unwrap().add(key, value);
512 Ok(self)
513 }
514
515 pub fn build(self) -> Result<ActivityDefinition, DataError> {
519 if self._name.is_none()
520 && self._description.is_none()
521 && self._type_.is_none()
522 && self._more_info.is_none()
523 && self._interaction_type.is_none()
524 && self._correct_responses_pattern.is_none()
525 && self._choices.is_none()
526 && self._scale.is_none()
527 && self._source.is_none()
528 && self._target.is_none()
529 && self._steps.is_none()
530 && self._extensions.is_none()
531 {
532 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
533 "At least 1 field must be set".into()
534 )))
535 }
536
537 if self._interaction_type.is_none()
538 && (self._correct_responses_pattern.is_some()
539 || self._choices.is_some()
540 || self._scale.is_some()
541 || self._source.is_some()
542 || self._target.is_some()
543 || self._steps.is_some())
544 {
545 emit_error!(DataError::Validation(ValidationError::MissingField(
546 "interaction_type".into()
547 )))
548 }
549
550 Ok(ActivityDefinition {
551 name: self._name,
552 description: self._description,
553 type_: self._type_.map(|x| x.into()),
554 more_info: self._more_info.map(|x| x.into()),
555 interaction_type: self._interaction_type,
556 correct_responses_pattern: self._correct_responses_pattern,
557 choices: self._choices,
558 scale: self._scale,
559 source: self._source,
560 target: self._target,
561 steps: self._steps,
562
563 extensions: self._extensions,
564 })
565 }
566}
567
568fn array_to_display_str(val: &[String]) -> String {
569 let mut vec = vec![];
570 for v in val.iter() {
571 vec.push(format!("\"{v}\""))
572 }
573 vec.iter()
574 .map(|x| x.to_string())
575 .collect::<Vec<_>>()
576 .join(", ")
577}
578
579fn vec_to_display_str(val: &Vec<InteractionComponent>) -> String {
580 let mut vec = vec![];
581 for ic in val {
582 vec.push(format!("{ic}"))
583 }
584 vec.iter()
585 .map(|x| x.to_string())
586 .collect::<Vec<_>>()
587 .join(", ")
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593 use tracing_test::traced_test;
594
595 #[traced_test]
596 #[test]
597 fn test_display() {
598 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" }"#;
599
600 let json = r#"{
601 "description": {
602 "en-US": "Does the xAPI include the concept of statements?"
603 },
604 "type": "http://adlnet.gov/expapi/activities/cmi.interaction",
605 "interactionType": "true-false",
606 "correctResponsesPattern": [
607 "true"
608 ]
609 }"#;
610
611 let de_result = serde_json::from_str::<ActivityDefinition>(json);
612 assert!(de_result.is_ok());
613 let ad = de_result.unwrap();
614 let display = format!("{}", ad);
615 assert_eq!(display, DISPLAY);
616 }
617
618 #[traced_test]
619 #[test]
620 fn test_missing_interaction_type() {
621 const BAD: &str = r#"{
622"name":{"en": "Fill-In"},
623"description":{"en": "Ben is often heard saying:"},
624"type":"http://adlnet.gov/expapi/activities/cmi.interaction",
625"moreInfo":"http://virtualmeeting.example.com/345256",
626"correctResponsesPattern":["Bob's your uncle"],
627"extensions":{
628 "http://example.com/profiles/meetings/extension/location":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one",
629 "http://example.com/profiles/meetings/extension/reporter":{"name":"Thomas","id":"http://openid.com/342"}
630}}"#;
631
632 let de_result = serde_json::from_str::<ActivityDefinition>(BAD);
633 assert!(de_result.is_ok());
634 let ad = de_result.unwrap();
635 assert!(!ad.is_valid());
637 }
638}