1use crate::{
4 Canonical, DataError, Extensions, InteractionComponent, InteractionType, LanguageMap,
5 MyLanguageTag, Validate, ValidationError, add_language, emit_error, merge_maps,
6 validate::validate_irl,
7};
8use core::fmt;
9use iri_string::types::{IriStr, IriString};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use serde_with::skip_serializing_none;
13
14#[skip_serializing_none]
17#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct ActivityDefinition {
20 name: Option<LanguageMap>,
21 description: Option<LanguageMap>,
22 #[serde(rename = "type")]
23 type_: Option<IriString>,
24 more_info: Option<IriString>,
25 interaction_type: Option<InteractionType>,
29 correct_responses_pattern: Option<Vec<String>>,
30 choices: Option<Vec<InteractionComponent>>,
31 scale: Option<Vec<InteractionComponent>>,
32 source: Option<Vec<InteractionComponent>>,
33 target: Option<Vec<InteractionComponent>>,
34 steps: Option<Vec<InteractionComponent>>,
35 extensions: Option<Extensions>,
36}
37
38impl ActivityDefinition {
39 pub fn builder() -> ActivityDefinitionBuilder<'static> {
41 ActivityDefinitionBuilder::default()
42 }
43
44 pub fn name(&self, tag: &MyLanguageTag) -> Option<&str> {
47 match &self.name {
48 Some(lm) => lm.get(tag),
49 None => None,
50 }
51 }
52
53 pub fn description(&self, tag: &MyLanguageTag) -> Option<&str> {
56 match &self.description {
57 Some(lm) => lm.get(tag),
58 None => None,
59 }
60 }
61
62 pub fn type_(&self) -> Option<&IriStr> {
64 self.type_.as_deref()
65 }
66
67 pub fn more_info(&self) -> Option<&IriStr> {
72 self.more_info.as_deref()
73 }
74
75 pub fn interaction_type(&self) -> Option<&InteractionType> {
88 self.interaction_type.as_ref()
89 }
90
91 pub fn correct_responses_pattern(&self) -> Option<&Vec<String>> {
98 self.correct_responses_pattern.as_ref()
99 }
100
101 pub fn choices(&self) -> Option<&Vec<InteractionComponent>> {
109 self.choices.as_ref()
110 }
111
112 pub fn scale(&self) -> Option<&Vec<InteractionComponent>> {
120 self.scale.as_ref()
121 }
122
123 pub fn source(&self) -> Option<&Vec<InteractionComponent>> {
131 self.source.as_ref()
132 }
133
134 pub fn target(&self) -> Option<&Vec<InteractionComponent>> {
142 self.target.as_ref()
143 }
144
145 pub fn steps(&self) -> Option<&Vec<InteractionComponent>> {
153 self.steps.as_ref()
154 }
155
156 pub fn extensions(&self) -> Option<&Extensions> {
158 self.extensions.as_ref()
159 }
160
161 pub fn extension(&self, key: &IriStr) -> Option<&Value> {
163 if let Some(z_extensions) = self.extensions.as_ref() {
164 z_extensions.get(key)
165 } else {
166 None
167 }
168 }
169
170 pub fn merge(&mut self, that: Self) {
172 fn merge_opt_collections(
174 dst: &mut Option<Vec<InteractionComponent>>,
175 src: Option<Vec<InteractionComponent>>,
176 ) {
177 match dst {
178 Some(lhs) => {
179 if let Some(rhs) = src {
180 InteractionComponent::merge_collections(lhs, rhs)
181 }
182 }
183 None => *dst = src,
184 }
185 }
186
187 merge_maps!(&mut self.name, that.name);
189 merge_maps!(&mut self.description, that.description);
190 merge_maps!(&mut self.extensions, that.extensions);
191 if self.type_.is_none() {
193 self.type_ = that.type_
194 }
195 if self.more_info.is_none() {
196 self.more_info = that.more_info
197 }
198 if self.interaction_type.is_none() {
199 self.interaction_type = that.interaction_type
200 }
201 match &mut self.correct_responses_pattern {
203 Some(this_field) => {
204 if let Some(that_field) = that.correct_responses_pattern {
205 this_field.extend(that_field);
206 this_field.sort();
208 this_field.dedup();
209 }
210 }
211 None => self.correct_responses_pattern = that.correct_responses_pattern,
212 }
213 merge_opt_collections(&mut self.choices, that.choices);
215 merge_opt_collections(&mut self.scale, that.scale);
216 merge_opt_collections(&mut self.source, that.source);
217 merge_opt_collections(&mut self.target, that.target);
218 merge_opt_collections(&mut self.steps, that.steps);
219 }
220}
221
222impl fmt::Display for ActivityDefinition {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 let mut vec = vec![];
225 if let Some(z_name) = self.name.as_ref() {
226 vec.push(format!("name: {}", z_name));
227 }
228 if let Some(z_description) = self.description.as_ref() {
229 vec.push(format!("description: {}", z_description));
230 }
231 if let Some(z_type) = self.type_.as_ref() {
232 vec.push(format!("type: \"{}\"", z_type));
233 }
234 if let Some(z_more_info) = self.more_info.as_ref() {
235 vec.push(format!("moreInfo: \"{}\"", z_more_info));
236 }
237 if let Some(z_interaction_type) = self.interaction_type.as_ref() {
238 vec.push(format!("interactionType: {}", z_interaction_type));
239 }
240 if let Some(z_correct_responses_pattern) = self.correct_responses_pattern.as_ref() {
241 vec.push(format!(
242 "correctResponsesPattern: {}",
243 array_to_display_str(z_correct_responses_pattern)
244 ));
245 }
246 if let Some(z_choices) = self.choices.as_ref() {
247 vec.push(format!("choices: {}", vec_to_display_str(z_choices)));
248 }
249 if let Some(z_scale) = self.scale.as_ref() {
250 vec.push(format!("scale: {}", vec_to_display_str(z_scale)));
251 }
252 if let Some(z_source) = self.source.as_ref() {
253 vec.push(format!("source: {}", vec_to_display_str(z_source)));
254 }
255 if let Some(z_target) = self.target.as_ref() {
256 vec.push(format!("target: {}", vec_to_display_str(z_target)));
257 }
258 if let Some(z_steps) = self.steps.as_ref() {
259 vec.push(format!("steps: {}", vec_to_display_str(z_steps)));
260 }
261 if let Some(z_extensions) = self.extensions.as_ref() {
262 vec.push(format!("extensions: {}", z_extensions))
263 }
264 let res = vec
265 .iter()
266 .map(|x| x.to_string())
267 .collect::<Vec<_>>()
268 .join(", ");
269 write!(f, "ActivityDefinition{{ {res} }}")
270 }
271}
272
273impl Validate for ActivityDefinition {
274 fn validate(&self) -> Vec<ValidationError> {
275 let mut vec: Vec<ValidationError> = vec![];
276
277 if self.type_.is_some() && self.type_.as_ref().unwrap().is_empty() {
279 vec.push(ValidationError::InvalidIRI("type".into()))
280 }
281 if let Some(z_more_info) = self.more_info.as_ref() {
283 validate_irl(z_more_info).unwrap_or_else(|x| vec.push(x));
284 }
285 if (self.correct_responses_pattern.is_some()
287 || self.choices.is_some()
288 || self.scale.is_some()
289 || self.source.is_some()
290 || self.target.is_some()
291 || self.steps.is_some())
292 && self.interaction_type.is_none()
293 {
294 vec.push(ValidationError::ConstraintViolation(
295 "Activity definition interaction-type must be present when any Interaction Activities properties is too".into(),
296 ))
297 }
298 if let Some(z_correct_responses_pattern) = self.correct_responses_pattern.as_ref() {
300 for it in z_correct_responses_pattern.iter() {
301 if it.is_empty() {
302 vec.push(ValidationError::Empty("correctResponsePattern".into()))
303 }
304 }
305 }
306 if let Some(z_choices) = self.choices.as_ref() {
308 z_choices.iter().for_each(|x| vec.extend(x.validate()));
309 }
310 if let Some(z_scale) = self.scale.as_ref() {
312 z_scale.iter().for_each(|x| vec.extend(x.validate()));
313 }
314 if let Some(z_source) = self.source.as_ref() {
316 z_source.iter().for_each(|x| vec.extend(x.validate()));
317 }
318 if let Some(z_target) = self.target.as_ref() {
320 z_target.iter().for_each(|x| vec.extend(x.validate()));
321 }
322 if let Some(z_steps) = self.steps.as_ref() {
324 z_steps.iter().for_each(|x| vec.extend(x.validate()));
325 }
326
327 vec
328 }
329}
330
331impl Canonical for ActivityDefinition {
332 fn canonicalize(&mut self, language_tags: &[MyLanguageTag]) {
333 if let Some(z_name) = self.name.as_mut() {
334 z_name.canonicalize(language_tags)
335 }
336 if let Some(z_description) = self.description.as_mut() {
337 z_description.canonicalize(language_tags)
338 }
339 if let Some(z_choices) = self.choices.as_mut() {
340 for it in z_choices {
341 it.canonicalize(language_tags)
342 }
343 }
344 if let Some(z_scale) = self.scale.as_mut() {
345 for it in z_scale {
346 it.canonicalize(language_tags)
347 }
348 }
349 if let Some(z_source) = self.source.as_mut() {
350 for it in z_source {
351 it.canonicalize(language_tags)
352 }
353 }
354 if let Some(z_target) = self.target.as_mut() {
355 for it in z_target {
356 it.canonicalize(language_tags)
357 }
358 }
359 if let Some(z_steps) = self.steps.as_mut() {
360 for it in z_steps {
361 it.canonicalize(language_tags)
362 }
363 }
364 }
365}
366
367#[derive(Debug, Default)]
369pub struct ActivityDefinitionBuilder<'a> {
370 _name: Option<LanguageMap>,
371 _description: Option<LanguageMap>,
372 _type_: Option<&'a IriStr>,
373 _more_info: Option<&'a IriStr>,
374 _interaction_type: Option<InteractionType>,
375 _correct_responses_pattern: Option<Vec<String>>,
376 _choices: Option<Vec<InteractionComponent>>,
377 _scale: Option<Vec<InteractionComponent>>,
378 _source: Option<Vec<InteractionComponent>>,
379 _target: Option<Vec<InteractionComponent>>,
380 _steps: Option<Vec<InteractionComponent>>,
381 _extensions: Option<Extensions>,
382}
383
384impl<'a> ActivityDefinitionBuilder<'a> {
385 pub fn name(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
389 add_language!(self._name, tag, label);
390 Ok(self)
391 }
392
393 pub fn description(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
398 add_language!(self._description, tag, label);
399 Ok(self)
400 }
401
402 pub fn type_(mut self, val: &'a str) -> Result<Self, DataError> {
404 let val = val.trim();
405 if val.is_empty() {
406 emit_error!(DataError::Validation(ValidationError::Empty("type".into())))
407 } else {
408 let iri = IriStr::new(val)?;
409 self._type_ = Some(iri);
410 Ok(self)
411 }
412 }
413
414 pub fn more_info(mut self, val: &'a str) -> Result<Self, DataError> {
416 let val = val.trim();
417 if val.is_empty() {
418 emit_error!(DataError::Validation(ValidationError::Empty(
419 "more_info".into()
420 )))
421 } else {
422 let val = IriStr::new(val)?;
423 validate_irl(val)?;
424 self._more_info = Some(val);
425 Ok(self)
426 }
427 }
428
429 pub fn interaction_type(mut self, val: InteractionType) -> Self {
431 self._interaction_type = Some(val);
432 self
433 }
434
435 pub fn correct_responses_pattern(mut self, val: &str) -> Result<Self, DataError> {
437 let val = val.trim();
438 if val.is_empty() {
439 emit_error!(DataError::Validation(ValidationError::Empty(
440 "correct_responses_pattern".into()
441 )))
442 }
443 if self._correct_responses_pattern.is_none() {
444 self._correct_responses_pattern = Some(vec![])
445 }
446 self._correct_responses_pattern
447 .as_mut()
448 .unwrap()
449 .push(val.to_string());
450 Ok(self)
451 }
452
453 pub fn choices(mut self, val: InteractionComponent) -> Result<Self, DataError> {
455 val.check_validity()?;
456 if self._choices.is_none() {
457 self._choices = Some(vec![])
458 }
459 self._choices.as_mut().unwrap().push(val);
460 Ok(self)
461 }
462
463 pub fn scale(mut self, val: InteractionComponent) -> Result<Self, DataError> {
465 val.check_validity()?;
466 if self._scale.is_none() {
467 self._scale = Some(vec![])
468 }
469 self._scale.as_mut().unwrap().push(val);
470 Ok(self)
471 }
472
473 pub fn source(mut self, val: InteractionComponent) -> Result<Self, DataError> {
475 val.check_validity()?;
476 if self._source.is_none() {
477 self._source = Some(vec![])
478 }
479 self._source.as_mut().unwrap().push(val);
480 Ok(self)
481 }
482
483 pub fn target(mut self, val: InteractionComponent) -> Result<Self, DataError> {
485 val.check_validity()?;
486 if self._target.is_none() {
487 self._target = Some(vec![])
488 }
489 self._target.as_mut().unwrap().push(val);
490 Ok(self)
491 }
492
493 pub fn steps(mut self, val: InteractionComponent) -> Result<Self, DataError> {
495 val.check_validity()?;
496 if self._steps.is_none() {
497 self._steps = Some(vec![])
498 }
499 self._steps.as_mut().unwrap().push(val);
500 Ok(self)
501 }
502
503 pub fn extension(mut self, key: &str, value: &Value) -> Result<Self, DataError> {
505 if self._extensions.is_none() {
506 self._extensions = Some(Extensions::new());
507 }
508 let _ = self._extensions.as_mut().unwrap().add(key, value);
509 Ok(self)
510 }
511
512 pub fn build(self) -> Result<ActivityDefinition, DataError> {
516 if self._name.is_none()
517 && self._description.is_none()
518 && self._type_.is_none()
519 && self._more_info.is_none()
520 && self._interaction_type.is_none()
521 && self._correct_responses_pattern.is_none()
522 && self._choices.is_none()
523 && self._scale.is_none()
524 && self._source.is_none()
525 && self._target.is_none()
526 && self._steps.is_none()
527 && self._extensions.is_none()
528 {
529 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
530 "At least 1 field must be set".into()
531 )))
532 }
533
534 if self._interaction_type.is_none()
535 && (self._correct_responses_pattern.is_some()
536 || self._choices.is_some()
537 || self._scale.is_some()
538 || self._source.is_some()
539 || self._target.is_some()
540 || self._steps.is_some())
541 {
542 emit_error!(DataError::Validation(ValidationError::MissingField(
543 "interaction_type".into()
544 )))
545 }
546
547 Ok(ActivityDefinition {
548 name: self._name,
549 description: self._description,
550 type_: self._type_.map(|x| x.into()),
551 more_info: self._more_info.map(|x| x.into()),
552 interaction_type: self._interaction_type,
553 correct_responses_pattern: self._correct_responses_pattern,
554 choices: self._choices,
555 scale: self._scale,
556 source: self._source,
557 target: self._target,
558 steps: self._steps,
559
560 extensions: self._extensions,
561 })
562 }
563}
564
565fn array_to_display_str(val: &[String]) -> String {
566 let mut vec = vec![];
567 for v in val.iter() {
568 vec.push(format!("\"{v}\""))
569 }
570 vec.iter()
571 .map(|x| x.to_string())
572 .collect::<Vec<_>>()
573 .join(", ")
574}
575
576fn vec_to_display_str(val: &Vec<InteractionComponent>) -> String {
577 let mut vec = vec![];
578 for ic in val {
579 vec.push(format!("{ic}"))
580 }
581 vec.iter()
582 .map(|x| x.to_string())
583 .collect::<Vec<_>>()
584 .join(", ")
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use tracing_test::traced_test;
591
592 #[traced_test]
593 #[test]
594 fn test_display() {
595 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" }"#;
596
597 let json = r#"{
598 "description": {
599 "en-US": "Does the xAPI include the concept of statements?"
600 },
601 "type": "http://adlnet.gov/expapi/activities/cmi.interaction",
602 "interactionType": "true-false",
603 "correctResponsesPattern": [
604 "true"
605 ]
606 }"#;
607
608 let de_result = serde_json::from_str::<ActivityDefinition>(json);
609 assert!(de_result.is_ok());
610 let ad = de_result.unwrap();
611 let display = format!("{}", ad);
612 assert_eq!(display, DISPLAY);
613 }
614
615 #[traced_test]
616 #[test]
617 fn test_missing_interaction_type() {
618 const BAD: &str = r#"{
619"name":{"en": "Fill-In"},
620"description":{"en": "Ben is often heard saying:"},
621"type":"http://adlnet.gov/expapi/activities/cmi.interaction",
622"moreInfo":"http://virtualmeeting.example.com/345256",
623"correctResponsesPattern":["Bob's your uncle"],
624"extensions":{
625 "http://example.com/profiles/meetings/extension/location":"X:\\\\meetings\\\\minutes\\\\examplemeeting.one",
626 "http://example.com/profiles/meetings/extension/reporter":{"name":"Thomas","id":"http://openid.com/342"}
627}}"#;
628
629 let de_result = serde_json::from_str::<ActivityDefinition>(BAD);
630 assert!(de_result.is_ok());
631 let ad = de_result.unwrap();
632 assert!(!ad.is_valid());
634 }
635}