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: value
69 .context_agents
70 .map(|z_agents| z_agents.into_iter().map(ContextAgentId::from).collect()),
71 context_groups: value
72 .context_groups
73 .map(|z_groups| z_groups.into_iter().map(ContextGroupId::from).collect()),
74 revision: value.revision,
75 platform: value.platform,
76 language: value.language,
77 statement: value.statement,
78 extensions: value.extensions,
79 }
80 }
81}
82
83impl From<ContextId> for Context {
84 fn from(value: ContextId) -> Self {
85 Context {
86 registration: value.registration,
87 instructor: value.instructor.map(Actor::from),
88 team: value.team.map(Group::from),
89 context_activities: value.context_activities.map(ContextActivities::from),
90 context_agents: value
91 .context_agents
92 .map(|z_agents| z_agents.into_iter().map(ContextAgent::from).collect()),
93 context_groups: value
94 .context_groups
95 .map(|z_groups| z_groups.into_iter().map(ContextGroup::from).collect()),
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 Context {
106 pub fn builder() -> ContextBuilder {
108 ContextBuilder::default()
109 }
110
111 pub fn registration(&self) -> Option<&Uuid> {
113 self.registration.as_ref()
114 }
115
116 pub fn instructor(&self) -> Option<&Actor> {
118 self.instructor.as_ref()
119 }
120
121 pub fn team(&self) -> Option<&Group> {
123 self.team.as_ref()
124 }
125
126 pub fn context_activities(&self) -> Option<&ContextActivities> {
128 self.context_activities.as_ref()
129 }
130
131 pub fn context_agents(&self) -> Option<&[ContextAgent]> {
133 self.context_agents.as_deref()
134 }
135
136 pub fn context_groups(&self) -> Option<&[ContextGroup]> {
138 self.context_groups.as_deref()
139 }
140
141 pub fn revision(&self) -> Option<&str> {
143 self.revision.as_deref()
144 }
145
146 pub fn platform(&self) -> Option<&str> {
148 self.platform.as_deref()
149 }
150
151 pub fn language(&self) -> Option<&MyLanguageTag> {
153 self.language.as_ref()
154 }
155
156 pub fn language_as_str(&self) -> Option<&str> {
158 match &self.language {
159 Some(x) => Some(x.as_str()),
160 None => None,
161 }
162 }
163
164 pub fn statement(&self) -> Option<&StatementRef> {
166 self.statement.as_ref()
167 }
168
169 pub fn extensions(&self) -> Option<&Extensions> {
171 self.extensions.as_ref()
172 }
173}
174
175impl Fingerprint for Context {
176 fn fingerprint<H: Hasher>(&self, state: &mut H) {
177 if self.registration.is_some() {
178 state.write(self.registration().unwrap().as_bytes());
179 }
180 if self.instructor.is_some() {
181 self.instructor().unwrap().fingerprint(state)
182 }
183 if self.team.is_some() {
184 self.team().unwrap().fingerprint(state)
185 }
186 if self.context_activities.is_some() {
187 self.context_activities().unwrap().fingerprint(state)
188 }
189 if self.context_agents.is_some() {
190 Fingerprint::fingerprint_slice(self.context_agents().unwrap(), state)
191 }
192 if self.context_groups.is_some() {
193 Fingerprint::fingerprint_slice(self.context_groups().unwrap(), state)
194 }
195 if self.revision.is_some() {
196 state.write(self.revision().unwrap().as_bytes())
197 }
198 if self.platform.is_some() {
199 state.write(self.platform().unwrap().as_bytes())
200 }
201 if let Some(z_language) = self.language.as_ref() {
202 state.write(z_language.as_str().as_bytes())
203 }
204 if self.statement.is_some() {
205 self.statement().unwrap().fingerprint(state)
206 }
207 if self.extensions.is_some() {
208 self.extensions().unwrap().fingerprint(state)
209 }
210 }
211}
212
213impl fmt::Display for Context {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 let mut vec = vec![];
216
217 if let Some(z_registration) = self.registration.as_ref() {
218 vec.push(format!(
219 "registration: \"{}\"",
220 z_registration
221 .hyphenated()
222 .encode_lower(&mut Uuid::encode_buffer())
223 ))
224 }
225 if let Some(z_instructor) = self.instructor.as_ref() {
226 vec.push(format!("instructor: {}", z_instructor))
227 }
228 if let Some(z_team) = self.team.as_ref() {
229 vec.push(format!("team: {}", z_team))
230 }
231 if let Some(z_activities) = self.context_activities.as_ref() {
232 vec.push(format!("contextActivities: {}", z_activities));
233 }
234 if self.context_agents.is_some() {
235 let items = self.context_agents.as_deref().unwrap();
236 vec.push(format!(
237 "contextAgents: [{}]",
238 items
239 .iter()
240 .map(|x| x.to_string())
241 .collect::<Vec<_>>()
242 .join(", ")
243 ));
244 }
245 if self.context_groups.is_some() {
246 let items = self.context_groups.as_deref().unwrap();
247 vec.push(format!(
248 "contextGroups: [{}]",
249 items
250 .iter()
251 .map(|x| x.to_string())
252 .collect::<Vec<_>>()
253 .join(", ")
254 ));
255 }
256 if let Some(z_revision) = self.revision.as_ref() {
257 vec.push(format!("revision: \"{}\"", z_revision))
258 }
259 if let Some(z_platform) = self.platform.as_ref() {
260 vec.push(format!("platform: \"{}\"", z_platform))
261 }
262 if let Some(z_language) = self.language.as_ref() {
263 vec.push(format!("language: \"{}\"", z_language))
264 }
265 if let Some(z_statement) = self.statement.as_ref() {
266 vec.push(format!("statement: {}", z_statement))
267 }
268 if let Some(z_extensions) = self.extensions.as_ref() {
269 vec.push(format!("extensions: {}", z_extensions))
270 }
271
272 let res = vec
273 .iter()
274 .map(|x| x.to_string())
275 .collect::<Vec<_>>()
276 .join(", ");
277 write!(f, "Context{{ {res} }}")
278 }
279}
280
281impl Validate for Context {
282 fn validate(&self) -> Vec<ValidationError> {
283 let mut vec = vec![];
284
285 if self.registration.is_some()
286 && (self.registration.as_ref().unwrap().is_nil()
287 || self.registration.as_ref().unwrap().is_max())
288 {
289 let msg = "UUID must not be all 0's or 1's";
290 error!("{}", msg);
291 vec.push(ValidationError::ConstraintViolation(msg.into()))
292 }
293 if let Some(z_instructor) = self.instructor.as_ref() {
294 vec.extend(z_instructor.validate())
295 }
296 if let Some(z_team) = self.team.as_ref() {
297 vec.extend(z_team.validate());
298 }
299 if let Some(z_activities) = self.context_activities.as_ref() {
300 vec.extend(z_activities.validate());
301 }
302 if let Some(z_agents) = self.context_agents.as_ref() {
303 for ca in z_agents.iter() {
304 vec.extend(ca.validate())
305 }
306 }
307 if let Some(z_groups) = self.context_groups.as_ref() {
308 for cg in z_groups.iter() {
309 vec.extend(cg.validate())
310 }
311 }
312 if self.revision.is_some() && self.revision.as_ref().unwrap().is_empty() {
313 vec.push(ValidationError::Empty("revision".into()))
314 }
315 if self.platform.is_some() && self.platform.as_ref().unwrap().is_empty() {
316 vec.push(ValidationError::Empty("platform".into()))
317 }
318 if let Some(z_statement) = self.statement.as_ref() {
319 vec.extend(z_statement.validate())
320 }
321
322 vec
323 }
324}
325
326#[derive(Debug, Default)]
328pub struct ContextBuilder {
329 _registration: Option<Uuid>,
330 _instructor: Option<Actor>,
331 _team: Option<Group>,
332 _context_activities: Option<ContextActivities>,
333 _context_agents: Option<Vec<ContextAgent>>,
334 _context_groups: Option<Vec<ContextGroup>>,
335 _revision: Option<String>,
336 _platform: Option<String>,
337 _language: Option<MyLanguageTag>,
338 _statement: Option<StatementRef>,
339 _extensions: Option<Extensions>,
340}
341
342impl ContextBuilder {
343 pub fn registration(mut self, val: &str) -> Result<Self, DataError> {
347 let val = val.trim();
348 if val.is_empty() {
349 emit_error!(DataError::Validation(ValidationError::Empty(
350 "registration".into()
351 )))
352 } else {
353 let uuid = Uuid::parse_str(val)?;
354 if uuid.is_nil() || uuid.is_max() {
355 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
356 "UUID should not be all zeroes or ones".into()
357 )))
358 } else {
359 self._registration = Some(uuid);
360 Ok(self)
361 }
362 }
363 }
364
365 pub fn registration_uuid(mut self, uuid: Uuid) -> Result<Self, DataError> {
369 if uuid.is_nil() || uuid.is_max() {
370 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
371 "UUID should not be all zeroes or ones".into()
372 )))
373 } else {
374 self._registration = Some(uuid);
375 Ok(self)
376 }
377 }
378
379 pub fn instructor(mut self, val: Actor) -> Result<Self, DataError> {
383 val.check_validity()?;
384 self._instructor = Some(val);
385 Ok(self)
386 }
387
388 pub fn team(mut self, val: Group) -> Result<Self, DataError> {
392 val.check_validity()?;
393 self._team = Some(val);
394 Ok(self)
395 }
396
397 pub fn context_activities(mut self, val: ContextActivities) -> Result<Self, DataError> {
401 val.check_validity()?;
402 self._context_activities = Some(val);
403 Ok(self)
404 }
405
406 pub fn context_agent(mut self, val: ContextAgent) -> Result<Self, DataError> {
410 val.check_validity()?;
411 if self._context_agents.is_none() {
412 self._context_agents = Some(vec![])
413 }
414 self._context_agents.as_mut().unwrap().push(val);
415 Ok(self)
416 }
417
418 pub fn context_group(mut self, val: ContextGroup) -> Result<Self, DataError> {
422 val.check_validity()?;
423 if self._context_groups.is_none() {
424 self._context_groups = Some(vec![])
425 }
426 self._context_groups.as_mut().unwrap().push(val);
427 Ok(self)
428 }
429
430 pub fn revision<S: Deref<Target = str>>(mut self, val: S) -> Result<Self, DataError> {
434 let val = val.trim();
435 if val.is_empty() {
436 emit_error!(DataError::Validation(ValidationError::Empty(
437 "revision".into()
438 )))
439 } else {
440 self._revision = Some(val.to_owned());
441 Ok(self)
442 }
443 }
444
445 pub fn platform<S: Deref<Target = str>>(mut self, val: S) -> Result<Self, DataError> {
449 let val = val.trim();
450 if val.is_empty() {
451 emit_error!(DataError::Validation(ValidationError::Empty(
452 "platform".into()
453 )))
454 } else {
455 self._platform = Some(val.to_owned());
456 Ok(self)
457 }
458 }
459
460 pub fn language<S: Deref<Target = str>>(mut self, val: S) -> Result<Self, DataError> {
464 let val = val.trim();
465 if val.is_empty() {
466 emit_error!(DataError::Validation(ValidationError::Empty(
467 "language".into()
468 )))
469 } else {
470 self._language = Some(MyLanguageTag::from_str(val)?);
471
472 Ok(self)
473 }
474 }
475
476 pub fn statement(mut self, val: StatementRef) -> Result<Self, DataError> {
480 val.check_validity()?;
481 self._statement = Some(val);
482 Ok(self)
483 }
484
485 pub fn statement_uuid(mut self, uuid: Uuid) -> Result<Self, DataError> {
489 let val = StatementRef::builder().id_as_uuid(uuid)?.build()?;
490 self._statement = Some(val);
491 Ok(self)
492 }
493
494 pub fn extension(mut self, key: &str, value: &Value) -> Result<Self, DataError> {
498 if self._extensions.is_none() {
499 self._extensions = Some(Extensions::new());
500 }
501 let _ = self._extensions.as_mut().unwrap().add(key, value);
502 Ok(self)
503 }
504
505 pub fn with_extensions(mut self, map: Extensions) -> Result<Self, DataError> {
508 self._extensions = Some(map);
509 Ok(self)
510 }
511
512 pub fn build(self) -> Result<Context, DataError> {
514 if self._registration.is_none()
515 && self._instructor.is_none()
516 && self._team.is_none()
517 && self._context_activities.is_none()
518 && self._context_agents.is_none()
519 && self._context_groups.is_none()
520 && self._revision.is_none()
521 && self._platform.is_none()
522 && self._language.is_none()
523 && self._statement.is_none()
524 && self._extensions.is_none()
525 {
526 emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
527 "At least one of the fields must not be empty".into()
528 )))
529 } else {
530 Ok(Context {
531 registration: self._registration,
532 instructor: self._instructor,
533 team: self._team,
534 context_activities: self._context_activities,
535 context_agents: self._context_agents,
536 context_groups: self._context_groups,
537 revision: self._revision,
538 platform: self._platform,
539 language: self._language,
540 statement: self._statement,
541 extensions: self._extensions,
542 })
543 }
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550 use tracing_test::traced_test;
551
552 #[traced_test]
553 #[test]
554 fn test_simple() {
555 const JSON: &str = r#"{
556 "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7",
557 "contextActivities": {
558 "parent": [
559 {
560 "id": "http://www.example.com/meetings/series/267",
561 "objectType": "Activity"
562 }
563 ],
564 "category": [
565 {
566 "id": "http://www.example.com/meetings/categories/teammeeting",
567 "objectType": "Activity",
568 "definition": {
569 "name": {
570 "en": "team meeting"
571 },
572 "description": {
573 "en": "A category of meeting used for regular team meetings."
574 },
575 "type": "http://example.com/expapi/activities/meetingcategory"
576 }
577 }
578 ],
579 "other": [
580 {
581 "id": "http://www.example.com/meetings/occurances/34257",
582 "objectType": "Activity"
583 },
584 {
585 "id": "http://www.example.com/meetings/occurances/3425567",
586 "objectType": "Activity"
587 }
588 ]
589 },
590 "instructor": {
591 "name": "Andrew Downes",
592 "account": {
593 "homePage": "http://www.example.com",
594 "name": "13936749"
595 },
596 "objectType": "Agent"
597 },
598 "team": {
599 "name": "Team PB",
600 "mbox": "mailto:teampb@example.com",
601 "objectType": "Group"
602 },
603 "platform": "Example virtual meeting software",
604 "language": "tlh",
605 "statement": {
606 "objectType": "StatementRef",
607 "id": "6690e6c9-3ef0-4ed3-8b37-7f3964730bee"
608 }
609 }"#;
610 let de_result = serde_json::from_str::<Context>(JSON);
611 assert!(de_result.is_ok());
612 let _ctx = de_result.unwrap();
613 }
614}