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 let Some(z_display) = self.display.as_ref() {
206 vec.push(format!("display: {}", z_display));
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 let Some(z_display) = self.display.as_mut() {
242 z_display.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 let Some(z_id) = self._id {
296 if z_id.is_empty() {
297 emit_error!(DataError::Validation(ValidationError::Empty("id".into())))
298 } else {
299 Ok(Verb {
300 id: z_id.into(),
301 display: self._display,
302 })
303 }
304 } else {
305 emit_error!(DataError::Validation(ValidationError::MissingField(
306 "id".into()
307 )))
308 }
309 }
310}
311
312static VERBS: OnceLock<HashMap<Vocabulary, Verb>> = OnceLock::new();
313fn verbs() -> &'static HashMap<Vocabulary, Verb> {
314 VERBS.get_or_init(|| {
315 HashMap::from([
316 (
317 Vocabulary::Answered,
318 Verb::from("http://adlnet.gov/expapi/verbs/answered").unwrap(),
319 ),
320 (
321 Vocabulary::Asked,
322 Verb::from("http://adlnet.gov/expapi/verbs/asked").unwrap(),
323 ),
324 (
325 Vocabulary::Attempted,
326 Verb::from("http://adlnet.gov/expapi/verbs/attempted").unwrap(),
327 ),
328 (
329 Vocabulary::Attended,
330 Verb::from("http://adlnet.gov/expapi/verbs/attended").unwrap(),
331 ),
332 (
333 Vocabulary::Commented,
334 Verb::from("http://adlnet.gov/expapi/verbs/commented").unwrap(),
335 ),
336 (
337 Vocabulary::Exited,
338 Verb::from("http://adlnet.gov/expapi/verbs/exited").unwrap(),
339 ),
340 (
341 Vocabulary::Experienced,
342 Verb::from("http://adlnet.gov/expapi/verbs/experienced").unwrap(),
343 ),
344 (
345 Vocabulary::Imported,
346 Verb::from("http://adlnet.gov/expapi/verbs/imported").unwrap(),
347 ),
348 (
349 Vocabulary::Interacted,
350 Verb::from("http://adlnet.gov/expapi/verbs/interacted").unwrap(),
351 ),
352 (
353 Vocabulary::Launched,
354 Verb::from("http://adlnet.gov/expapi/verbs/launched").unwrap(),
355 ),
356 (
357 Vocabulary::Mastered,
358 Verb::from("http://adlnet.gov/expapi/verbs/mastered").unwrap(),
359 ),
360 (
361 Vocabulary::Preferred,
362 Verb::from("http://adlnet.gov/expapi/verbs/preferred").unwrap(),
363 ),
364 (
365 Vocabulary::Progressed,
366 Verb::from("http://adlnet.gov/expapi/verbs/progressed").unwrap(),
367 ),
368 (
369 Vocabulary::Registered,
370 Verb::from("http://adlnet.gov/expapi/verbs/registered").unwrap(),
371 ),
372 (
373 Vocabulary::Shared,
374 Verb::from("http://adlnet.gov/expapi/verbs/shared").unwrap(),
375 ),
376 (
377 Vocabulary::Voided,
378 Verb::from("http://adlnet.gov/expapi/verbs/voided").unwrap(),
379 ),
380 (
381 Vocabulary::LoggedIn,
382 Verb::from("http://adlnet.gov/expapi/verbs/logged-in").unwrap(),
383 ),
384 (
385 Vocabulary::LoggedOut,
386 Verb::from("http://adlnet.gov/expapi/verbs/logged-out").unwrap(),
387 ),
388 ])
389 })
390}
391
392fn is_adl_verb(iri: &IriStr) -> Option<&Verb> {
393 if let Some(verb) = verbs().values().find(|&x| x.id() == iri) {
394 Some(verb)
395 } else {
396 None
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use iri_string::{format::ToDedicatedString, spec::IriSpec, validate::iri};
404 use std::str::FromStr;
405 use tracing_test::traced_test;
406 use url::Url;
407
408 const JSON: &str =
409 r#"{"id": "http://adlnet.gov/expapi/verbs/logged-out","display": {"en": "logged-out"}}"#;
410
411 #[traced_test]
412 #[test]
413 fn test_serde() {
414 let v1 = adl_verb(Vocabulary::LoggedIn);
416 let json_result = serde_json::to_string(v1);
417 assert!(json_result.is_ok());
418 let json = json_result.unwrap();
419 let v1_result = serde_json::from_str::<Verb>(&json);
420 assert!(v1_result.is_ok());
421 let v11 = v1_result.unwrap();
422 assert_eq!(v11.id.as_str(), "http://adlnet.gov/expapi/verbs/logged-in");
423
424 let v2 = Verb::from("ftp://example.net/whatever").unwrap();
425 let json_result = serde_json::to_string(&v2);
426 assert!(json_result.is_ok());
427 let json = json_result.unwrap();
428 assert!(!json.contains("display"));
430 }
431
432 #[test]
433 fn test_deserialization() -> Result<(), DataError> {
434 let de_result = serde_json::from_str::<Verb>(JSON);
435 assert!(de_result.is_ok());
436 let v = de_result.unwrap();
437
438 let url = Url::parse("http://adlnet.gov/expapi/verbs/logged-out").unwrap();
439 assert_eq!(url.as_str(), v.id());
440 assert!(v.display.is_some());
441 let en_result = v.display(&MyLanguageTag::from_str("en")?);
442 assert!(en_result.is_some());
443 assert_eq!(en_result.unwrap(), "logged-out");
444
445 Ok(())
446 }
447
448 #[test]
449 fn test_display() {
450 const DISPLAY: &str = r#"Verb{ id: "http://adlnet.gov/expapi/verbs/logged-out", display: {"en":"logged-out"} }"#;
451
452 let de_result = serde_json::from_str::<Verb>(JSON);
453 let v = de_result.unwrap();
454 let display = format!("{}", v);
455 assert_eq!(display, DISPLAY);
456 }
457
458 #[test]
459 fn test_eq() {
460 let de_result = serde_json::from_str::<Verb>(JSON);
461 let v1 = de_result.unwrap();
462
463 assert_ne!(&v1, adl_verb(Vocabulary::LoggedOut));
467 assert!(v1.equivalent(adl_verb(Vocabulary::LoggedOut)));
469
470 assert_ne!(&v1, adl_verb(Vocabulary::LoggedIn));
472 assert!(!v1.equivalent(adl_verb(Vocabulary::LoggedIn)));
473
474 let v3 = Verb::from("http://adlnet.gov/expapi/verbs/logged-out").unwrap();
475 assert_ne!(v1, v3);
477 assert!(v1.equivalent(&v3));
479 }
480
481 #[traced_test]
482 #[test]
483 fn test_normalized() {
484 let iri = IriStr::new("HTTP://example.COM/foo/./bar/%2e%2e/../baz?query#fragment").unwrap();
485 let normalized = iri.normalize().to_dedicated_string();
486 assert_eq!(normalized, "http://example.com/baz?query#fragment");
487
488 let iri = IriStr::new("HTTP://Résumé.example.ORG").unwrap();
489 let normalized = iri.normalize().to_dedicated_string();
490 assert_eq!(normalized, "http://Résumé.example.ORG");
493 }
494
495 #[traced_test]
496 #[test]
497 fn test_validation() {
498 const IRI1_STR: &str = "HTTP://Résumé.example.ORG";
499 const IRI2_STR: &str = "http://résumé.example.org";
500
501 let v1 = Verb::from(IRI1_STR).unwrap();
502 let r1 = v1.validate();
503 assert!(r1.is_empty());
504
505 let v2 = Verb::from(IRI2_STR).unwrap();
506 let r2 = v2.validate();
507 assert!(r2.is_empty());
508
509 assert_ne!(v1, v2);
510
511 assert!(iri::<IriSpec>(IRI1_STR).is_ok());
513 assert!(iri::<IriSpec>(IRI2_STR).is_ok());
514 }
515}