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