use crate::{
Canonical, DataError, Extensions, InteractionComponent, InteractionType, LanguageMap,
MyLanguageTag, Validate, ValidationError, add_language, emit_error, merge_maps,
validate::validate_irl,
};
use core::fmt;
use iri_string::types::{IriStr, IriString};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::skip_serializing_none;
#[skip_serializing_none]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ActivityDefinition {
name: Option<LanguageMap>,
description: Option<LanguageMap>,
#[serde(rename = "type")]
type_: Option<IriString>,
more_info: Option<IriString>,
interaction_type: Option<InteractionType>,
correct_responses_pattern: Option<Vec<String>>,
choices: Option<Vec<InteractionComponent>>,
scale: Option<Vec<InteractionComponent>>,
source: Option<Vec<InteractionComponent>>,
target: Option<Vec<InteractionComponent>>,
steps: Option<Vec<InteractionComponent>>,
extensions: Option<Extensions>,
}
impl ActivityDefinition {
pub fn builder() -> ActivityDefinitionBuilder<'static> {
ActivityDefinitionBuilder::default()
}
pub fn name(&self, tag: &MyLanguageTag) -> Option<&str> {
match &self.name {
Some(lm) => lm.get(tag),
None => None,
}
}
pub fn description(&self, tag: &MyLanguageTag) -> Option<&str> {
match &self.description {
Some(lm) => lm.get(tag),
None => None,
}
}
pub fn type_(&self) -> Option<&IriStr> {
self.type_.as_deref()
}
pub fn more_info(&self) -> Option<&IriStr> {
self.more_info.as_deref()
}
pub fn interaction_type(&self) -> Option<&InteractionType> {
self.interaction_type.as_ref()
}
pub fn correct_responses_pattern(&self) -> Option<&Vec<String>> {
self.correct_responses_pattern.as_ref()
}
pub fn choices(&self) -> Option<&Vec<InteractionComponent>> {
self.choices.as_ref()
}
pub fn scale(&self) -> Option<&Vec<InteractionComponent>> {
self.scale.as_ref()
}
pub fn source(&self) -> Option<&Vec<InteractionComponent>> {
self.source.as_ref()
}
pub fn target(&self) -> Option<&Vec<InteractionComponent>> {
self.target.as_ref()
}
pub fn steps(&self) -> Option<&Vec<InteractionComponent>> {
self.steps.as_ref()
}
pub fn extensions(&self) -> Option<&Extensions> {
self.extensions.as_ref()
}
pub fn extension(&self, key: &IriStr) -> Option<&Value> {
if let Some(z_extensions) = self.extensions.as_ref() {
z_extensions.get(key)
} else {
None
}
}
pub fn merge(&mut self, that: Self) {
fn merge_opt_collections(
dst: &mut Option<Vec<InteractionComponent>>,
src: Option<Vec<InteractionComponent>>,
) {
match dst {
Some(lhs) => {
if let Some(rhs) = src {
InteractionComponent::merge_collections(lhs, rhs)
}
}
None => *dst = src,
}
}
merge_maps!(&mut self.name, that.name);
merge_maps!(&mut self.description, that.description);
merge_maps!(&mut self.extensions, that.extensions);
if self.type_.is_none() {
self.type_ = that.type_
}
if self.more_info.is_none() {
self.more_info = that.more_info
}
if self.interaction_type.is_none() {
self.interaction_type = that.interaction_type
}
match &mut self.correct_responses_pattern {
Some(this_field) => {
if let Some(that_field) = that.correct_responses_pattern {
this_field.extend(that_field);
this_field.sort();
this_field.dedup();
}
}
None => self.correct_responses_pattern = that.correct_responses_pattern,
}
merge_opt_collections(&mut self.choices, that.choices);
merge_opt_collections(&mut self.scale, that.scale);
merge_opt_collections(&mut self.source, that.source);
merge_opt_collections(&mut self.target, that.target);
merge_opt_collections(&mut self.steps, that.steps);
}
}
impl fmt::Display for ActivityDefinition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut vec = vec![];
if let Some(z_name) = self.name.as_ref() {
vec.push(format!("name: {}", z_name));
}
if let Some(z_description) = self.description.as_ref() {
vec.push(format!("description: {}", z_description));
}
if let Some(z_type) = self.type_.as_ref() {
vec.push(format!("type: \"{}\"", z_type));
}
if let Some(z_more_info) = self.more_info.as_ref() {
vec.push(format!("moreInfo: \"{}\"", z_more_info));
}
if let Some(z_interaction_type) = self.interaction_type.as_ref() {
vec.push(format!("interactionType: {}", z_interaction_type));
}
if let Some(z_correct_responses_pattern) = self.correct_responses_pattern.as_ref() {
vec.push(format!(
"correctResponsesPattern: {}",
array_to_display_str(z_correct_responses_pattern)
));
}
if let Some(z_choices) = self.choices.as_ref() {
vec.push(format!("choices: {}", vec_to_display_str(z_choices)));
}
if let Some(z_scale) = self.scale.as_ref() {
vec.push(format!("scale: {}", vec_to_display_str(z_scale)));
}
if let Some(z_source) = self.source.as_ref() {
vec.push(format!("source: {}", vec_to_display_str(z_source)));
}
if let Some(z_target) = self.target.as_ref() {
vec.push(format!("target: {}", vec_to_display_str(z_target)));
}
if let Some(z_steps) = self.steps.as_ref() {
vec.push(format!("steps: {}", vec_to_display_str(z_steps)));
}
if let Some(z_extensions) = self.extensions.as_ref() {
vec.push(format!("extensions: {}", z_extensions))
}
let res = vec
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "ActivityDefinition{{ {res} }}")
}
}
impl Validate for ActivityDefinition {
fn validate(&self) -> Vec<ValidationError> {
let mut vec: Vec<ValidationError> = vec![];
if self.type_.is_some() && self.type_.as_ref().unwrap().is_empty() {
vec.push(ValidationError::InvalidIRI("type".into()))
}
if let Some(z_more_info) = self.more_info.as_ref() {
validate_irl(z_more_info).unwrap_or_else(|x| vec.push(x));
}
if (self.correct_responses_pattern.is_some()
|| self.choices.is_some()
|| self.scale.is_some()
|| self.source.is_some()
|| self.target.is_some()
|| self.steps.is_some())
&& self.interaction_type.is_none()
{
vec.push(ValidationError::ConstraintViolation(
"Activity definition interaction-type must be present when any Interaction Activities properties is too".into(),
))
}
if let Some(z_correct_responses_pattern) = self.correct_responses_pattern.as_ref() {
for it in z_correct_responses_pattern.iter() {
if it.is_empty() {
vec.push(ValidationError::Empty("correctResponsePattern".into()))
}
}
}
if let Some(z_choices) = self.choices.as_ref() {
z_choices.iter().for_each(|x| vec.extend(x.validate()));
}
if let Some(z_scale) = self.scale.as_ref() {
z_scale.iter().for_each(|x| vec.extend(x.validate()));
}
if let Some(z_source) = self.source.as_ref() {
z_source.iter().for_each(|x| vec.extend(x.validate()));
}
if let Some(z_target) = self.target.as_ref() {
z_target.iter().for_each(|x| vec.extend(x.validate()));
}
if let Some(z_steps) = self.steps.as_ref() {
z_steps.iter().for_each(|x| vec.extend(x.validate()));
}
vec
}
}
impl Canonical for ActivityDefinition {
fn canonicalize(&mut self, language_tags: &[MyLanguageTag]) {
if let Some(z_name) = self.name.as_mut() {
z_name.canonicalize(language_tags)
}
if let Some(z_description) = self.description.as_mut() {
z_description.canonicalize(language_tags)
}
if let Some(z_choices) = self.choices.as_mut() {
for it in z_choices {
it.canonicalize(language_tags)
}
}
if let Some(z_scale) = self.scale.as_mut() {
for it in z_scale {
it.canonicalize(language_tags)
}
}
if let Some(z_source) = self.source.as_mut() {
for it in z_source {
it.canonicalize(language_tags)
}
}
if let Some(z_target) = self.target.as_mut() {
for it in z_target {
it.canonicalize(language_tags)
}
}
if let Some(z_steps) = self.steps.as_mut() {
for it in z_steps {
it.canonicalize(language_tags)
}
}
}
}
#[derive(Debug, Default)]
pub struct ActivityDefinitionBuilder<'a> {
_name: Option<LanguageMap>,
_description: Option<LanguageMap>,
_type_: Option<&'a IriStr>,
_more_info: Option<&'a IriStr>,
_interaction_type: Option<InteractionType>,
_correct_responses_pattern: Option<Vec<String>>,
_choices: Option<Vec<InteractionComponent>>,
_scale: Option<Vec<InteractionComponent>>,
_source: Option<Vec<InteractionComponent>>,
_target: Option<Vec<InteractionComponent>>,
_steps: Option<Vec<InteractionComponent>>,
_extensions: Option<Extensions>,
}
impl<'a> ActivityDefinitionBuilder<'a> {
pub fn name(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
add_language!(self._name, tag, label);
Ok(self)
}
pub fn description(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
add_language!(self._description, tag, label);
Ok(self)
}
pub fn type_(mut self, val: &'a str) -> Result<Self, DataError> {
let val = val.trim();
if val.is_empty() {
emit_error!(DataError::Validation(ValidationError::Empty("type".into())))
} else {
let iri = IriStr::new(val)?;
self._type_ = Some(iri);
Ok(self)
}
}
pub fn more_info(mut self, val: &'a str) -> Result<Self, DataError> {
let val = val.trim();
if val.is_empty() {
emit_error!(DataError::Validation(ValidationError::Empty(
"more_info".into()
)))
} else {
let val = IriStr::new(val)?;
validate_irl(val)?;
self._more_info = Some(val);
Ok(self)
}
}
pub fn interaction_type(mut self, val: InteractionType) -> Self {
self._interaction_type = Some(val);
self
}
pub fn correct_responses_pattern(mut self, val: &str) -> Result<Self, DataError> {
let val = val.trim();
if val.is_empty() {
emit_error!(DataError::Validation(ValidationError::Empty(
"correct_responses_pattern".into()
)))
}
if self._correct_responses_pattern.is_none() {
self._correct_responses_pattern = Some(vec![])
}
self._correct_responses_pattern
.as_mut()
.unwrap()
.push(val.to_string());
Ok(self)
}
pub fn choices(mut self, val: InteractionComponent) -> Result<Self, DataError> {
val.check_validity()?;
if self._choices.is_none() {
self._choices = Some(vec![])
}
self._choices.as_mut().unwrap().push(val);
Ok(self)
}
pub fn scale(mut self, val: InteractionComponent) -> Result<Self, DataError> {
val.check_validity()?;
if self._scale.is_none() {
self._scale = Some(vec![])
}
self._scale.as_mut().unwrap().push(val);
Ok(self)
}
pub fn source(mut self, val: InteractionComponent) -> Result<Self, DataError> {
val.check_validity()?;
if self._source.is_none() {
self._source = Some(vec![])
}
self._source.as_mut().unwrap().push(val);
Ok(self)
}
pub fn target(mut self, val: InteractionComponent) -> Result<Self, DataError> {
val.check_validity()?;
if self._target.is_none() {
self._target = Some(vec![])
}
self._target.as_mut().unwrap().push(val);
Ok(self)
}
pub fn steps(mut self, val: InteractionComponent) -> Result<Self, DataError> {
val.check_validity()?;
if self._steps.is_none() {
self._steps = Some(vec![])
}
self._steps.as_mut().unwrap().push(val);
Ok(self)
}
pub fn extension(mut self, key: &str, value: &Value) -> Result<Self, DataError> {
if self._extensions.is_none() {
self._extensions = Some(Extensions::new());
}
let _ = self._extensions.as_mut().unwrap().add(key, value);
Ok(self)
}
pub fn build(self) -> Result<ActivityDefinition, DataError> {
if self._name.is_none()
&& self._description.is_none()
&& self._type_.is_none()
&& self._more_info.is_none()
&& self._interaction_type.is_none()
&& self._correct_responses_pattern.is_none()
&& self._choices.is_none()
&& self._scale.is_none()
&& self._source.is_none()
&& self._target.is_none()
&& self._steps.is_none()
&& self._extensions.is_none()
{
emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
"At least 1 field must be set".into()
)))
}
if self._interaction_type.is_none()
&& (self._correct_responses_pattern.is_some()
|| self._choices.is_some()
|| self._scale.is_some()
|| self._source.is_some()
|| self._target.is_some()
|| self._steps.is_some())
{
emit_error!(DataError::Validation(ValidationError::MissingField(
"interaction_type".into()
)))
}
Ok(ActivityDefinition {
name: self._name,
description: self._description,
type_: self._type_.map(|x| x.into()),
more_info: self._more_info.map(|x| x.into()),
interaction_type: self._interaction_type,
correct_responses_pattern: self._correct_responses_pattern,
choices: self._choices,
scale: self._scale,
source: self._source,
target: self._target,
steps: self._steps,
extensions: self._extensions,
})
}
}
fn array_to_display_str(val: &[String]) -> String {
let mut vec = vec![];
for v in val.iter() {
vec.push(format!("\"{v}\""))
}
vec.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
}
fn vec_to_display_str(val: &Vec<InteractionComponent>) -> String {
let mut vec = vec![];
for ic in val {
vec.push(format!("{ic}"))
}
vec.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
use tracing_test::traced_test;
#[traced_test]
#[test]
fn test_display() {
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" }"#;
let json = r#"{
"description": {
"en-US": "Does the xAPI include the concept of statements?"
},
"type": "http://adlnet.gov/expapi/activities/cmi.interaction",
"interactionType": "true-false",
"correctResponsesPattern": [
"true"
]
}"#;
let de_result = serde_json::from_str::<ActivityDefinition>(json);
assert!(de_result.is_ok());
let ad = de_result.unwrap();
let display = format!("{}", ad);
assert_eq!(display, DISPLAY);
}
#[traced_test]
#[test]
fn test_missing_interaction_type() {
const BAD: &str = r#"{
"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"}
}}"#;
let de_result = serde_json::from_str::<ActivityDefinition>(BAD);
assert!(de_result.is_ok());
let ad = de_result.unwrap();
assert!(!ad.is_valid());
}
}