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