xapi_rs/data/
context_agent.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::{
4    AgentId, DataError,
5    data::{Agent, Fingerprint, ObjectType, Validate, ValidationError},
6    emit_error,
7};
8use core::fmt;
9use iri_string::types::{IriStr, IriString};
10use serde::{Deserialize, Serialize};
11use serde_with::skip_serializing_none;
12use std::hash::{Hash, Hasher};
13
14/// Structure for capturing a relationship between a [Statement][1] and one or
15/// more [Agent][2](s) --besides the [Actor][3]-- in order to properly describe
16/// an experience.
17///
18/// [1]: crate::Statement
19/// [2]: crate::Agent
20/// [3]: crate::Actor
21#[skip_serializing_none]
22#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
23#[serde(deny_unknown_fields)]
24#[serde(rename_all = "camelCase")]
25pub struct ContextAgent {
26    #[serde(default = "default_object_type")]
27    object_type: ObjectType,
28    agent: Agent,
29    // IMPORTANT (rsn) 20241023 - Following comments in [2] i'm now changing
30    // this type and removing the `Option` wrapper. The validation logic now
31    // rejects instances w/ empty collections.
32    // IMPORTANT (rsn) 20241017 - The [specs][1] under _Context Agents Table_
33    // as well as _Context Group Table_ describe this field as optional. However
34    // they are described as ...a collection of 1 or more Relevant Type(s) used
35    // to characterize the relationship between the Statement and the Actor. If
36    // not provided, only a generic relationship is intended (not recommended).
37    //
38    // [1]: https://opensource.ieee.org/xapi/xapi-base-standard-documentation/-/blob/main/9274.1.1%20xAPI%20Base%20Standard%20for%20LRSs.md#4225-context
39    // [2]: https://github.com/adlnet/lrs-conformance-test-suite/issues/279
40    // relevant_types: Option<Vec<IriString>>,
41    relevant_types: Vec<IriString>,
42}
43
44#[skip_serializing_none]
45#[derive(Debug, Serialize)]
46#[serde(rename_all = "camelCase")]
47pub(crate) struct ContextAgentId {
48    object_type: ObjectType,
49    agent: AgentId,
50    relevant_types: Vec<IriString>,
51}
52
53impl From<ContextAgent> for ContextAgentId {
54    fn from(value: ContextAgent) -> Self {
55        ContextAgentId {
56            object_type: ObjectType::ContextAgent,
57            agent: AgentId::from(value.agent),
58            relevant_types: value.relevant_types,
59        }
60    }
61}
62
63impl From<ContextAgentId> for ContextAgent {
64    fn from(value: ContextAgentId) -> Self {
65        ContextAgent {
66            object_type: ObjectType::ContextAgent,
67            agent: Agent::from(value.agent),
68            relevant_types: value.relevant_types,
69        }
70    }
71}
72
73impl ContextAgent {
74    /// Return a [ContextAgent] _Builder_
75    pub fn builder() -> ContextAgentBuilder {
76        ContextAgentBuilder::default()
77    }
78
79    /// Return TRUE if the `objectType` property is [ContextAgent][1]; FALSE
80    /// otherwise.
81    ///
82    /// [1]: ObjectType#variant.ContextAgent
83    pub fn check_object_type(&self) -> bool {
84        self.object_type == ObjectType::ContextAgent
85    }
86
87    /// Return `agent` field.
88    pub fn agent(&self) -> &Agent {
89        &self.agent
90    }
91
92    /// Return `relevant_types` field as an array of IRIs.
93    pub fn relevant_types(&self) -> &[IriString] {
94        self.relevant_types.as_ref()
95    }
96}
97
98impl Fingerprint for ContextAgent {
99    fn fingerprint<H: Hasher>(&self, state: &mut H) {
100        self.agent.fingerprint(state);
101        for s in &self.relevant_types {
102            s.hash(state)
103        }
104    }
105}
106
107impl fmt::Display for ContextAgent {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        let mut vec = vec![];
110
111        vec.push(format!("agent: {}", self.agent));
112        vec.push(format!(
113            "relevantTypes: [{}]",
114            self.relevant_types
115                .iter()
116                .map(|x| x.to_string())
117                .collect::<Vec<_>>()
118                .join(", ")
119        ));
120
121        let res = vec
122            .iter()
123            .map(|x| x.to_string())
124            .collect::<Vec<_>>()
125            .join(", ");
126        write!(f, "ContextAgent{{ {res} }}")
127    }
128}
129
130impl Validate for ContextAgent {
131    fn validate(&self) -> Vec<ValidationError> {
132        let mut vec = vec![];
133
134        if !self.check_object_type() {
135            vec.push(ValidationError::WrongObjectType {
136                expected: ObjectType::ContextAgent,
137                found: self.object_type.to_string().into(),
138            })
139        }
140        vec.extend(self.agent.validate());
141        if self.relevant_types.is_empty() {
142            vec.push(ValidationError::Empty("relevant_types".into()))
143        } else {
144            for iri in self.relevant_types.iter() {
145                if iri.is_empty() {
146                    vec.push(ValidationError::InvalidIRI(iri.to_string().into()))
147                }
148            }
149        }
150
151        vec
152    }
153}
154
155/// A Type that knows how to construct a [ContextAgent].
156#[derive(Debug, Default)]
157pub struct ContextAgentBuilder {
158    _agent: Option<Agent>,
159    _relevant_types: Vec<IriString>,
160}
161
162impl ContextAgentBuilder {
163    /// Set the `agent` field.
164    ///
165    /// Raise [DataError] if [Agent] argument is invalid.
166    pub fn agent(mut self, val: Agent) -> Result<Self, DataError> {
167        val.check_validity()?;
168        self._agent = Some(val);
169        Ok(self)
170    }
171
172    /// Add IRI string to `relevant_types` collection if it's not empty.
173    ///
174    /// Raise [DataError] if it is.
175    pub fn relevant_type(mut self, val: &str) -> Result<Self, DataError> {
176        if val.is_empty() {
177            emit_error!(DataError::Validation(ValidationError::Empty(
178                "relevant-type IRI".into()
179            )))
180        } else {
181            let iri = IriStr::new(val)?;
182            if self._relevant_types.is_empty() {
183                self._relevant_types = vec![];
184            }
185            self._relevant_types.push(iri.to_owned());
186            Ok(self)
187        }
188    }
189
190    /// Construct a [ContextAgent] instance.
191    ///
192    /// Raise [DataError] if `agent` field is not set.
193    pub fn build(self) -> Result<ContextAgent, DataError> {
194        if self._agent.is_none() {
195            emit_error!(DataError::Validation(ValidationError::MissingField(
196                "agent".into()
197            )))
198        } else if self._relevant_types.is_empty() {
199            emit_error!(DataError::Validation(ValidationError::Empty(
200                "relevant_types".into()
201            )))
202        } else {
203            let mut relevant_types = vec![];
204            for item in self._relevant_types.iter() {
205                let iri = IriString::try_from(item.as_str())?;
206                relevant_types.push(iri);
207            }
208
209            Ok(ContextAgent {
210                object_type: ObjectType::ContextAgent,
211                agent: self._agent.unwrap(),
212                relevant_types,
213            })
214        }
215    }
216}
217
218fn default_object_type() -> ObjectType {
219    ObjectType::ContextAgent
220}