Skip to main content

xapi_data/
context_agent.rs

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