1use crate::{
4 Actor, ActorId, ContextActivities, ContextActivitiesId, ContextAgent, ContextAgentId,
5 ContextGroup, ContextGroupId, DataError, Extensions, Fingerprint, Group, GroupId,
6 MyLanguageTag, StatementRef, Validate, ValidationError, emit_error,
7};
8use core::fmt;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use serde_with::skip_serializing_none;
12use std::{hash::Hasher, ops::Deref, str::FromStr};
13use tracing::error;
14use uuid::Uuid;
15
16#[skip_serializing_none]
23#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
24#[serde(deny_unknown_fields)]
25#[serde(rename_all = "camelCase")]
26pub struct Context {
27 registration: Option<Uuid>,
28 instructor: Option<Actor>,
29 team: Option<Group>,
30 context_activities: Option<ContextActivities>,
31 context_agents: Option<Vec<ContextAgent>>,
32 context_groups: Option<Vec<ContextGroup>>,
33 revision: Option<String>,
34 platform: Option<String>,
35 language: Option<MyLanguageTag>,
36 statement: Option<StatementRef>,
37 extensions: Option<Extensions>,
38}
39
40#[skip_serializing_none]
41#[derive(Debug, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub(crate) struct ContextId {
44 registration: Option<Uuid>,
45 instructor: Option<ActorId>,
46 team: Option<GroupId>,
47 context_activities: Option<ContextActivitiesId>,
48 context_agents: Option<Vec<ContextAgentId>>,
49 context_groups: Option<Vec<ContextGroupId>>,
50 revision: Option<String>,
51 platform: Option<String>,
52 language: Option<MyLanguageTag>,
53 statement: Option<StatementRef>,
54 extensions: Option<Extensions>,
55}
56
57impl From<Context> for ContextId {
58 fn from(value: Context) -> Self {
59 ContextId {
60 registration: value.registration,
61 instructor: value.instructor.map(ActorId::from),
62 team: value.team.map(GroupId::from),
63 context_activities: value.context_activities.map(ContextActivitiesId::from),
64 context_agents: value
65 .context_agents
66 .map(|z_agents| z_agents.into_iter().map(ContextAgentId::from).collect()),
67 context_groups: value
68 .context_groups
69 .map(|z_groups| z_groups.into_iter().map(ContextGroupId::from).collect()),
70 revision: value.revision,
71 platform: value.platform,
72 language: value.language,
73 statement: value.statement,
74 extensions: value.extensions,
75 }
76 }
77}
78
79impl From<ContextId> for Context {
80 fn from(value: ContextId) -> Self {
81 Context {
82 registration: value.registration,
83 instructor: value.instructor.map(Actor::from),
84 team: value.team.map(Group::from),
85 context_activities: value.context_activities.map(ContextActivities::from),
86 context_agents: value
87 .context_agents
88 .map(|z_agents| z_agents.into_iter().map(ContextAgent::from).collect()),
89 context_groups: value
90 .context_groups
91 .map(|z_groups| z_groups.into_iter().map(ContextGroup::from).collect()),
92 revision: value.revision,
93 platform: value.platform,
94 language: value.language,
95 statement: value.statement,
96 extensions: value.extensions,
97 }
98 }
99}
100
101impl Context {
102 pub fn builder() -> ContextBuilder {
104 ContextBuilder::default()
105 }
106
107 pub fn registration(&self) -> Option<&Uuid> {
109 self.registration.as_ref()
110 }
111
112 pub fn instructor(&self) -> Option<&Actor> {
114 self.instructor.as_ref()
115 }
116
117 pub fn team(&self) -> Option<&Group> {
119 self.team.as_ref()
120 }
121
122 pub fn context_activities(&self) -> Option<&ContextActivities> {
124 self.context_activities.as_ref()
125 }
126
127 pub fn context_agents(&self) -> Option<&[ContextAgent]> {
129 self.context_agents.as_deref()
130 }
131
132 pub fn context_groups(&self) -> Option<&[ContextGroup]> {
134 self.context_groups.as_deref()
135 }
136
137 pub fn revision(&self) -> Option<&str> {
139 self.revision.as_deref()
140 }
141
142 pub fn platform(&self) -> Option<&str> {
144 self.platform.as_deref()
145 }
146
147 pub fn language(&self) -> Option<&MyLanguageTag> {
149 self.language.as_ref()
150 }
151
152 pub fn language_as_str(&self) -> Option<&str> {
154 match &self.language {
155 Some(x) => Some(x.as_str()),
156 None => None,
157 }
158 }
159
160 pub fn statement(&self) -> Option<&StatementRef> {
162 self.statement.as_ref()
163 }
164
165 pub fn extensions(&self) -> Option<&Extensions> {
167 self.extensions.as_ref()
168 }
169}
170
171impl Fingerprint for Context {
172 fn fingerprint<H: Hasher>(&self, state: &mut H) {
173 if self.registration.is_some() {
174 state.write(self.registration().unwrap().as_bytes());
175 }
176 if self.instructor.is_some() {
177 self.instructor().unwrap().fingerprint(state)
178 }
179 if self.team.is_some() {
180 self.team().unwrap().fingerprint(state)
181 }
182 if self.context_activities.is_some() {
183 self.context_activities().unwrap().fingerprint(state)
184 }
185 if self.context_agents.is_some() {
186 Fingerprint::fingerprint_slice(self.context_agents().unwrap(), state)
187 }
188 if self.context_groups.is_some() {
189 Fingerprint::fingerprint_slice(self.context_groups().unwrap(), state)
190 }
191 if self.revision.is_some() {
192 state.write(self.revision().unwrap().as_bytes())
193 }
194 if self.platform.is_some() {
195 state.write(self.platform().unwrap().as_bytes())
196 }
197 if let Some(z_language) = self.language.as_ref() {
198 state.write(z_language.as_str().as_bytes())
199 }
200 if self.statement.is_some() {
201 self.statement().unwrap().fingerprint(state)
202 }
203 if self.extensions.is_some() {
204 self.extensions().unwrap().fingerprint(state)
205 }
206 }
207}
208
209impl fmt::Display for Context {
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 let mut vec = vec![];
212
213 if let Some(z_registration) = self.registration.as_ref() {
214 vec.push(format!(
215 "registration: \"{}\"",
216 z_registration
217 .hyphenated()
218 .encode_lower(&mut Uuid::encode_buffer())
219 ))
220 }
221 if let Some(z_instructor) = self.instructor.as_ref() {
222 vec.push(format!("instructor: {}", z_instructor))
223 }
224 if let Some(z_team) = self.team.as_ref() {
225 vec.push(format!("team: {}", z_team))
226 }
227 if let Some(z_activities) = self.context_activities.as_ref() {
228 vec.push(format!("contextActivities: {}", z_activities));
229 }
230 if self.context_agents.is_some() {
231 let items = self.context_agents.as_deref().unwrap();
232 vec.push(format!(
233 "contextAgents: [{}]",
234 items
235 .iter()
236 .map(|x| x.to_string())
237 .collect::<Vec<_>>()
238 .join(", ")
239 ));
240 }
241 if self.context_groups.is_some() {
242 let items = self.context_groups.as_deref().unwrap();
243 vec.push(format!(
244 "contextGroups: [{}]",
245 items
246 .iter()
247 .map(|x| x.to_string())
248 .collect::<Vec<_>>()
249 .join(", ")
250 ));
251 }
252 if let Some(z_revision) = self.revision.as_ref() {
253 vec.push(format!("revision: \"{}\"", z_revision))
254 }
255 if let Some(z_platform) = self.platform.as_ref() {
256 vec.push(format!("platform: \"{}\"", z_platform))
257 }
258 if let Some(z_language) = self.language.as_ref() {
259 vec.push(format!("language: \"{}\"", z_language))
260 }
261 if let Some(z_statement) = self.statement.as_ref() {
262 vec.push(format!("statement: {}", z_statement))
263 }
264 if let Some(z_extensions) = self.extensions.as_ref() {
265 vec.push(format!("extensions: {}", z_extensions))
266 }
267
268 let res = vec
269 .iter()
270 .map(|x| x.to_string())
271 .collect::<Vec<_>>()
272 .join(", ");
273 write!(f, "Context{{ {res} }}")
274 }
275}
276
277impl Validate for Context {
278 fn validate(&self) -> Vec<ValidationError> {
279 let mut vec = vec![];
280
281 if self.registration.is_some()
282 && (self.registration.as_ref().unwrap().is_nil()
283 || self.registration.as_ref().unwrap().is_max())
284 {
285 let msg = "UUID must not be all 0's or 1's";
286 error!("{}", msg);
287 vec.push(ValidationError::ConstraintViolation(msg.into()))
288 }
289 if let Some(z_instructor) = self.instructor.as_ref() {
290 vec.extend(z_instructor.validate())
291 }
292 if let Some(z_team) = self.team.as_ref() {
293 vec.extend(z_team.validate());
294 }
295 if let Some(z_activities) = self.context_activities.as_ref() {
296 vec.extend(z_activities.validate());
297 }
298 if let Some(z_agents) = self.context_agents.as_ref() {
299 for ca in z_agents.iter() {
300 vec.extend(ca.validate())
301 }
302 }
303 if let Some(z_groups) = self.context_groups.as_ref() {
304 for cg in z_groups.iter() {
305 vec.extend(cg.validate())
306 }
307 }
308 if self.revision.is_some() && self.revision.as_ref().unwrap().is_empty() {
309 vec.push(ValidationError::Empty("revision".into()))
310 }
311 if self.platform.is_some() && self.platform.as_ref().unwrap().is_empty() {
312 vec.push(ValidationError::Empty("platform".into()))
313 }
314 if let Some(z_statement) = self.statement.as_ref() {
315 vec.extend(z_statement.validate())
316 }
317
318 vec
319 }
320}
321
322#[derive(Debug, Default)]
324pub struct ContextBuilder {
325 _registration: Option<Uuid>,
326 _instructor: Option<Actor>,
327 _team: Option<Group>,
328 _context_activities: Option<ContextActivities>,
329 _context_agents: Option<Vec<ContextAgent>>,
330 _context_groups: Option<Vec<ContextGroup>>,
331 _revision: Option<String>,
332 _platform: Option<String>,
333 _language: Option<MyLanguageTag>,
334 _statement: Option<StatementRef>,
335 _extensions: Option<Extensions>,
336}
337
338impl ContextBuilder {
339 pub fn registration(mut self, val: &str) -> Result<Self, DataError> {
343 let val = val.trim();
344 if val.is_empty() {
345 emit_error!(DataError::Validation(ValidationError::Empty(
346 "registration".into()
347 )))
348 } else {
349 let uuid = Uuid::parse_str(val)?;
350 if uuid.is_nil() || uuid.is_max() {
351 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
352 "UUID should not be all zeroes or ones".into()
353 )))
354 } else {
355 self._registration = Some(uuid);
356 Ok(self)
357 }
358 }
359 }
360
361 pub fn registration_uuid(mut self, uuid: Uuid) -> Result<Self, DataError> {
365 if uuid.is_nil() || uuid.is_max() {
366 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
367 "UUID should not be all zeroes or ones".into()
368 )))
369 } else {
370 self._registration = Some(uuid);
371 Ok(self)
372 }
373 }
374
375 pub fn instructor(mut self, val: Actor) -> Result<Self, DataError> {
379 val.check_validity()?;
380 self._instructor = Some(val);
381 Ok(self)
382 }
383
384 pub fn team(mut self, val: Group) -> Result<Self, DataError> {
388 val.check_validity()?;
389 self._team = Some(val);
390 Ok(self)
391 }
392
393 pub fn context_activities(mut self, val: ContextActivities) -> Result<Self, DataError> {
397 val.check_validity()?;
398 self._context_activities = Some(val);
399 Ok(self)
400 }
401
402 pub fn context_agent(mut self, val: ContextAgent) -> Result<Self, DataError> {
406 val.check_validity()?;
407 if self._context_agents.is_none() {
408 self._context_agents = Some(vec![])
409 }
410 self._context_agents.as_mut().unwrap().push(val);
411 Ok(self)
412 }
413
414 pub fn context_group(mut self, val: ContextGroup) -> Result<Self, DataError> {
418 val.check_validity()?;
419 if self._context_groups.is_none() {
420 self._context_groups = Some(vec![])
421 }
422 self._context_groups.as_mut().unwrap().push(val);
423 Ok(self)
424 }
425
426 pub fn revision<S: Deref<Target = str>>(mut self, val: S) -> Result<Self, DataError> {
430 let val = val.trim();
431 if val.is_empty() {
432 emit_error!(DataError::Validation(ValidationError::Empty(
433 "revision".into()
434 )))
435 } else {
436 self._revision = Some(val.to_owned());
437 Ok(self)
438 }
439 }
440
441 pub fn platform<S: Deref<Target = str>>(mut self, val: S) -> Result<Self, DataError> {
445 let val = val.trim();
446 if val.is_empty() {
447 emit_error!(DataError::Validation(ValidationError::Empty(
448 "platform".into()
449 )))
450 } else {
451 self._platform = Some(val.to_owned());
452 Ok(self)
453 }
454 }
455
456 pub fn language<S: Deref<Target = str>>(mut self, val: S) -> Result<Self, DataError> {
460 let val = val.trim();
461 if val.is_empty() {
462 emit_error!(DataError::Validation(ValidationError::Empty(
463 "language".into()
464 )))
465 } else {
466 self._language = Some(MyLanguageTag::from_str(val)?);
467
468 Ok(self)
469 }
470 }
471
472 pub fn statement(mut self, val: StatementRef) -> Result<Self, DataError> {
476 val.check_validity()?;
477 self._statement = Some(val);
478 Ok(self)
479 }
480
481 pub fn statement_uuid(mut self, uuid: Uuid) -> Result<Self, DataError> {
485 let val = StatementRef::builder().id_as_uuid(uuid)?.build()?;
486 self._statement = Some(val);
487 Ok(self)
488 }
489
490 pub fn extension(mut self, key: &str, value: &Value) -> Result<Self, DataError> {
494 if self._extensions.is_none() {
495 self._extensions = Some(Extensions::new());
496 }
497 let _ = self._extensions.as_mut().unwrap().add(key, value);
498 Ok(self)
499 }
500
501 pub fn with_extensions(mut self, map: Extensions) -> Result<Self, DataError> {
504 self._extensions = Some(map);
505 Ok(self)
506 }
507
508 pub fn build(self) -> Result<Context, DataError> {
510 if self._registration.is_none()
511 && self._instructor.is_none()
512 && self._team.is_none()
513 && self._context_activities.is_none()
514 && self._context_agents.is_none()
515 && self._context_groups.is_none()
516 && self._revision.is_none()
517 && self._platform.is_none()
518 && self._language.is_none()
519 && self._statement.is_none()
520 && self._extensions.is_none()
521 {
522 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
523 "At least one of the fields must not be empty".into()
524 )))
525 } else {
526 Ok(Context {
527 registration: self._registration,
528 instructor: self._instructor,
529 team: self._team,
530 context_activities: self._context_activities,
531 context_agents: self._context_agents,
532 context_groups: self._context_groups,
533 revision: self._revision,
534 platform: self._platform,
535 language: self._language,
536 statement: self._statement,
537 extensions: self._extensions,
538 })
539 }
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use tracing_test::traced_test;
547
548 #[traced_test]
549 #[test]
550 fn test_simple() {
551 const JSON: &str = r#"{
552 "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7",
553 "contextActivities": {
554 "parent": [
555 {
556 "id": "http://www.example.com/meetings/series/267",
557 "objectType": "Activity"
558 }
559 ],
560 "category": [
561 {
562 "id": "http://www.example.com/meetings/categories/teammeeting",
563 "objectType": "Activity",
564 "definition": {
565 "name": {
566 "en": "team meeting"
567 },
568 "description": {
569 "en": "A category of meeting used for regular team meetings."
570 },
571 "type": "http://example.com/expapi/activities/meetingcategory"
572 }
573 }
574 ],
575 "other": [
576 {
577 "id": "http://www.example.com/meetings/occurances/34257",
578 "objectType": "Activity"
579 },
580 {
581 "id": "http://www.example.com/meetings/occurances/3425567",
582 "objectType": "Activity"
583 }
584 ]
585 },
586 "instructor": {
587 "name": "Andrew Downes",
588 "account": {
589 "homePage": "http://www.example.com",
590 "name": "13936749"
591 },
592 "objectType": "Agent"
593 },
594 "team": {
595 "name": "Team PB",
596 "mbox": "mailto:teampb@example.com",
597 "objectType": "Group"
598 },
599 "platform": "Example virtual meeting software",
600 "language": "tlh",
601 "statement": {
602 "objectType": "StatementRef",
603 "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee"
604 }
605 }"#;
606 let de_result = serde_json::from_str::<Context>(JSON);
607 assert!(de_result.is_ok());
608 let _ctx = de_result.unwrap();
609 }
610}