1use crate::{
4 add_language,
5 data::{
6 Canonical, DataError, Fingerprint, LanguageMap, MyLanguageTag, Validate, ValidationError,
7 fingerprint_it,
8 },
9 emit_error,
10};
11use core::fmt;
12use iri_string::types::{IriStr, IriString};
13use serde::{Deserialize, Serialize};
14use serde_with::skip_serializing_none;
15use std::{
16 collections::HashMap,
17 hash::{Hash, Hasher},
18 sync::OnceLock,
19};
20
21#[derive(Debug, Eq, Hash, PartialEq)]
26pub enum Vocabulary {
27 Answered,
31 Asked,
34 Attempted,
38 Attended,
41 Commented,
44 Exited,
46 Experienced,
49 Imported,
52 Interacted,
54 Launched,
56 Mastered,
59 Preferred,
62 Progressed,
65 Registered,
67 Shared,
70 Voided,
74 LoggedIn,
77 LoggedOut,
80}
81
82pub fn adl_verb(id: Vocabulary) -> &'static Verb {
84 verbs().get(&id).unwrap()
85}
86
87#[skip_serializing_none]
93#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
94#[serde(deny_unknown_fields)]
95#[serde(rename_all = "camelCase")]
96pub struct Verb {
97 id: IriString,
98 display: Option<LanguageMap>,
99}
100
101#[derive(Debug, Serialize)]
102pub(crate) struct VerbId {
103 id: IriString,
104}
105
106impl From<Verb> for VerbId {
107 fn from(value: Verb) -> Self {
108 VerbId { id: value.id }
109 }
110}
111
112impl From<VerbId> for Verb {
113 fn from(value: VerbId) -> Self {
114 Verb {
115 id: value.id,
116 display: None,
117 }
118 }
119}
120
121impl Verb {
122 fn from(id: &str) -> Result<Self, DataError> {
123 let iri = IriStr::new(id)?;
124 Ok(Verb {
125 id: iri.into(),
126 display: None,
127 })
128 }
129
130 pub fn builder() -> VerbBuilder<'static> {
132 VerbBuilder::default()
133 }
134
135 pub fn id(&self) -> &IriStr {
137 &self.id
138 }
139
140 pub fn id_as_str(&self) -> &str {
142 self.id.as_str()
143 }
144
145 pub fn is_voided(&self) -> bool {
147 self.id.eq(adl_verb(Vocabulary::Voided).id())
148 }
149
150 pub fn display(&self, tag: &MyLanguageTag) -> Option<&str> {
155 match &self.display {
156 Some(lm) => lm.get(tag),
157 None => None,
158 }
159 }
160
161 pub fn display_as_map(&self) -> Option<&LanguageMap> {
164 self.display.as_ref()
165 }
166
167 pub fn uid(&self) -> u64 {
169 fingerprint_it(self)
170 }
171
172 pub fn equivalent(&self, that: &Verb) -> bool {
174 self.uid() == that.uid()
175 }
176
177 pub fn extend(&mut self, other: Verb) -> bool {
184 match (&self.display, other.display) {
185 (_, None) => false,
186 (None, Some(y)) => {
187 self.display = Some(y);
188 true
189 }
190 (Some(x), Some(y)) => {
191 let mut old_display = x.to_owned();
192 old_display.extend(y);
193 self.display = Some(old_display);
194 true
195 }
196 }
197 }
198}
199
200impl fmt::Display for Verb {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 let mut vec = vec![];
203
204 vec.push(format!("id: \"{}\"", self.id));
205 if self.display.is_some() {
206 vec.push(format!("display: {}", self.display.as_ref().unwrap()));
207 }
208
209 let res = vec
210 .iter()
211 .map(|x| x.to_string())
212 .collect::<Vec<_>>()
213 .join(", ");
214 write!(f, "Verb{{ {res} }}")
215 }
216}
217
218impl Fingerprint for Verb {
219 fn fingerprint<H: Hasher>(&self, state: &mut H) {
220 let (x, y) = self.id.as_slice().to_absolute_and_fragment();
221 x.normalize().to_string().hash(state);
222 y.hash(state);
223 }
225}
226
227impl Validate for Verb {
228 fn validate(&self) -> Vec<ValidationError> {
229 let mut vec = vec![];
230
231 if self.id.is_empty() {
232 vec.push(ValidationError::Empty("id".into()))
233 }
234
235 vec
236 }
237}
238
239impl Canonical for Verb {
240 fn canonicalize(&mut self, tags: &[MyLanguageTag]) {
241 if self.display.is_some() {
242 self.display.as_mut().unwrap().canonicalize(tags);
243 }
244 }
245}
246
247#[derive(Debug, Default)]
249pub struct VerbBuilder<'a> {
250 _id: Option<&'a IriStr>,
251 _display: Option<LanguageMap>,
252}
253
254impl<'a> VerbBuilder<'a> {
255 pub fn id(mut self, val: &'a str) -> Result<Self, DataError> {
260 let id = val.trim();
261 if id.is_empty() {
262 emit_error!(DataError::Validation(ValidationError::Empty("id".into())))
263 } else {
264 let iri = IriStr::new(id)?;
265 if let Some(v) = is_adl_verb(iri) {
267 self._id = Some(&v.id)
268 } else {
269 self._id = Some(iri);
270 }
271 Ok(self)
272 }
273 }
274
275 pub fn display(mut self, tag: &MyLanguageTag, label: &str) -> Result<Self, DataError> {
279 add_language!(self._display, tag, label);
280 Ok(self)
281 }
282
283 pub fn with_display(mut self, map: LanguageMap) -> Result<Self, DataError> {
286 self._display = Some(map);
287 Ok(self)
288 }
289
290 pub fn build(self) -> Result<Verb, DataError> {
295 if self._id.is_none() {
296 emit_error!(DataError::Validation(ValidationError::MissingField(
297 "id".into()
298 )))
299 } else {
300 let iri = self._id.unwrap();
301 if iri.is_empty() {
302 emit_error!(DataError::Validation(ValidationError::Empty("id".into())))
303 } else {
304 Ok(Verb {
305 id: self._id.unwrap().into(),
306 display: self._display,
307 })
308 }
309 }
310 }
311}
312
313static VERBS: OnceLock<HashMap<Vocabulary, Verb>> = OnceLock::new();
314fn verbs() -> &'static HashMap<Vocabulary, Verb> {
315 VERBS.get_or_init(|| {
316 HashMap::from([
317 (
318 Vocabulary::Answered,
319 Verb::from("http://adlnet.gov/expapi/verbs/answered").unwrap(),
320 ),
321 (
322 Vocabulary::Asked,
323 Verb::from("http://adlnet.gov/expapi/verbs/asked").unwrap(),
324 ),
325 (
326 Vocabulary::Attempted,
327 Verb::from("http://adlnet.gov/expapi/verbs/attempted").unwrap(),
328 ),
329 (
330 Vocabulary::Attended,
331 Verb::from("http://adlnet.gov/expapi/verbs/attended").unwrap(),
332 ),
333 (
334 Vocabulary::Commented,
335 Verb::from("http://adlnet.gov/expapi/verbs/commented").unwrap(),
336 ),
337 (
338 Vocabulary::Exited,
339 Verb::from("http://adlnet.gov/expapi/verbs/exited").unwrap(),
340 ),
341 (
342 Vocabulary::Experienced,
343 Verb::from("http://adlnet.gov/expapi/verbs/experienced").unwrap(),
344 ),
345 (
346 Vocabulary::Imported,
347 Verb::from("http://adlnet.gov/expapi/verbs/imported").unwrap(),
348 ),
349 (
350 Vocabulary::Interacted,
351 Verb::from("http://adlnet.gov/expapi/verbs/interacted").unwrap(),
352 ),
353 (
354 Vocabulary::Launched,
355 Verb::from("http://adlnet.gov/expapi/verbs/launched").unwrap(),
356 ),
357 (
358 Vocabulary::Mastered,
359 Verb::from("http://adlnet.gov/expapi/verbs/mastered").unwrap(),
360 ),
361 (
362 Vocabulary::Preferred,
363 Verb::from("http://adlnet.gov/expapi/verbs/preferred").unwrap(),
364 ),
365 (
366 Vocabulary::Progressed,
367 Verb::from("http://adlnet.gov/expapi/verbs/progressed").unwrap(),
368 ),
369 (
370 Vocabulary::Registered,
371 Verb::from("http://adlnet.gov/expapi/verbs/registered").unwrap(),
372 ),
373 (
374 Vocabulary::Shared,
375 Verb::from("http://adlnet.gov/expapi/verbs/shared").unwrap(),
376 ),
377 (
378 Vocabulary::Voided,
379 Verb::from("http://adlnet.gov/expapi/verbs/voided").unwrap(),
380 ),
381 (
382 Vocabulary::LoggedIn,
383 Verb::from("http://adlnet.gov/expapi/verbs/logged-in").unwrap(),
384 ),
385 (
386 Vocabulary::LoggedOut,
387 Verb::from("http://adlnet.gov/expapi/verbs/logged-out").unwrap(),
388 ),
389 ])
390 })
391}
392
393fn is_adl_verb(iri: &IriStr) -> Option<&Verb> {
394 if let Some(verb) = verbs().values().find(|&x| x.id() == iri) {
395 Some(verb)
396 } else {
397 None
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use iri_string::{format::ToDedicatedString, spec::IriSpec, validate::iri};
405 use std::str::FromStr;
406 use tracing_test::traced_test;
407 use url::Url;
408
409 const JSON: &str =
410 r#"{"id": "http://adlnet.gov/expapi/verbs/logged-out","display": {"en": "logged-out"}}"#;
411
412 #[traced_test]
413 #[test]
414 fn test_serde() {
415 let v1 = adl_verb(Vocabulary::LoggedIn);
417 let json_result = serde_json::to_string(v1);
418 assert!(json_result.is_ok());
419 let json = json_result.unwrap();
420 let v1_result = serde_json::from_str::<Verb>(&json);
421 assert!(v1_result.is_ok());
422 let v11 = v1_result.unwrap();
423 assert_eq!(v11.id.as_str(), "http://adlnet.gov/expapi/verbs/logged-in");
424
425 let v2 = Verb::from("ftp://example.net/whatever").unwrap();
426 let json_result = serde_json::to_string(&v2);
427 assert!(json_result.is_ok());
428 let json = json_result.unwrap();
429 assert!(!json.contains("display"));
431 }
432
433 #[test]
434 fn test_deserialization() -> Result<(), DataError> {
435 let de_result = serde_json::from_str::<Verb>(JSON);
436 assert!(de_result.is_ok());
437 let v = de_result.unwrap();
438
439 let url = Url::parse("http://adlnet.gov/expapi/verbs/logged-out").unwrap();
440 assert_eq!(url.as_str(), v.id());
441 assert!(v.display.is_some());
442 let en_result = v.display(&MyLanguageTag::from_str("en")?);
443 assert!(en_result.is_some());
444 assert_eq!(en_result.unwrap(), "logged-out");
445
446 Ok(())
447 }
448
449 #[test]
450 fn test_display() {
451 const DISPLAY: &str = r#"Verb{ id: "http://adlnet.gov/expapi/verbs/logged-out", display: {"en":"logged-out"} }"#;
452
453 let de_result = serde_json::from_str::<Verb>(JSON);
454 let v = de_result.unwrap();
455 let display = format!("{}", v);
456 assert_eq!(display, DISPLAY);
457 }
458
459 #[test]
460 fn test_eq() {
461 let de_result = serde_json::from_str::<Verb>(JSON);
462 let v1 = de_result.unwrap();
463
464 assert_ne!(&v1, adl_verb(Vocabulary::LoggedOut));
468 assert!(v1.equivalent(adl_verb(Vocabulary::LoggedOut)));
470
471 assert_ne!(&v1, adl_verb(Vocabulary::LoggedIn));
473 assert!(!v1.equivalent(adl_verb(Vocabulary::LoggedIn)));
474
475 let v3 = Verb::from("http://adlnet.gov/expapi/verbs/logged-out").unwrap();
476 assert_ne!(v1, v3);
478 assert!(v1.equivalent(&v3));
480 }
481
482 #[traced_test]
483 #[test]
484 fn test_normalized() {
485 let iri = IriStr::new("HTTP://example.COM/foo/./bar/%2e%2e/../baz?query#fragment").unwrap();
486 let normalized = iri.normalize().to_dedicated_string();
487 assert_eq!(normalized, "http://example.com/baz?query#fragment");
488
489 let iri = IriStr::new("HTTP://Résumé.example.ORG").unwrap();
490 let normalized = iri.normalize().to_dedicated_string();
491 assert_eq!(normalized, "http://Résumé.example.ORG");
494 }
495
496 #[traced_test]
497 #[test]
498 fn test_validation() {
499 const IRI1_STR: &str = "HTTP://Résumé.example.ORG";
500 const IRI2_STR: &str = "http://résumé.example.org";
501
502 let v1 = Verb::from(IRI1_STR).unwrap();
503 let r1 = v1.validate();
504 assert!(r1.is_empty());
505
506 let v2 = Verb::from(IRI2_STR).unwrap();
507 let r2 = v2.validate();
508 assert!(r2.is_empty());
509
510 assert_ne!(v1, v2);
511
512 assert!(iri::<IriSpec>(IRI1_STR).is_ok());
514 assert!(iri::<IriSpec>(IRI2_STR).is_ok());
515 }
516}