#![allow(clippy::unwrap_used, clippy::expect_used)]
use iri_rs::{Iri, IriBuf, iri};
use jsonld::{Expand, LoadError, Loader, RemoteDocument, syntax::Parse};
use std::{
collections::HashMap,
sync::atomic::{AtomicUsize, Ordering},
};
struct CountingLoader {
documents: HashMap<IriBuf, RemoteDocument>,
loads: AtomicUsize,
}
impl CountingLoader {
fn new(contexts: &[(Iri<&str>, &str)]) -> Self {
let documents = contexts
.iter()
.map(|(url, body)| {
let (json, _) = jsonld::syntax::Value::parse_str(body).unwrap();
let url = IriBuf::new(url.as_str().to_owned()).unwrap();
(url.clone(), RemoteDocument::new(Some(url), None, json))
})
.collect();
Self {
documents,
loads: AtomicUsize::new(0),
}
}
fn loads(&self) -> usize {
self.loads.load(Ordering::Relaxed)
}
}
impl Loader for CountingLoader {
type Error = <HashMap<IriBuf, RemoteDocument> as Loader>::Error;
async fn load(&self, url: Iri<&str>) -> Result<RemoteDocument<IriBuf>, LoadError<Self::Error>> {
self.loads.fetch_add(1, Ordering::Relaxed);
self.documents.load(url).await
}
}
const A: Iri<&str> = iri!("http://example.com/a.jsonld");
const B: Iri<&str> = iri!("http://example.com/b.jsonld");
const A_DOCUMENT: &str = r#"{"@context": {"t": "http://example.com/a#t"}}"#;
const B_DOCUMENT: &str = r#"{"@context": {"t": "http://example.com/b#t"}}"#;
async fn expanded_properties(input: &str, loader: &CountingLoader) -> Vec<String> {
let (json, _) = jsonld::syntax::Value::parse_str(input).unwrap();
let expanded = json.expand(loader).await.unwrap();
expanded
.iter()
.flat_map(|object| object.as_node().into_iter())
.flat_map(|node| node.properties().iter().map(|(property, _)| property.as_str().to_owned()))
.collect()
}
#[tokio::test]
async fn a_context_repeated_at_the_top_level_is_processed_again() {
let loader = CountingLoader::new(&[(A, A_DOCUMENT), (B, B_DOCUMENT)]);
let input = format!(r#"{{"@context": ["{A}", "{B}", "{A}"], "@id": "http://example.com/n", "t": "v"}}"#);
let properties = expanded_properties(&input, &loader).await;
assert_eq!(properties, vec!["http://example.com/a#t".to_owned()]);
}
#[tokio::test]
async fn a_context_repeated_at_the_top_level_is_dereferenced_once() {
let loader = CountingLoader::new(&[(A, A_DOCUMENT), (B, B_DOCUMENT)]);
let input = format!(r#"{{"@context": ["{A}", "{B}", "{A}"], "@id": "http://example.com/n", "t": "v"}}"#);
expanded_properties(&input, &loader).await;
assert_eq!(loader.loads(), 2);
}
#[tokio::test]
async fn the_last_occurrence_of_a_repeated_context_decides() {
let loader = CountingLoader::new(&[(A, A_DOCUMENT), (B, B_DOCUMENT)]);
let input = format!(r#"{{"@context": ["{B}", "{A}", "{B}"], "@id": "http://example.com/n", "t": "v"}}"#);
let properties = expanded_properties(&input, &loader).await;
assert_eq!(properties, vec!["http://example.com/b#t".to_owned()]);
}
#[tokio::test]
async fn a_scoped_context_naming_its_own_document_terminates() {
const SELF_SCOPED: Iri<&str> = iri!("http://example.com/self.jsonld");
let document = format!(r#"{{"@context": {{"t": {{"@id": "http://example.com/s#t", "@context": "{SELF_SCOPED}"}}}}}}"#);
let loader = CountingLoader::new(&[(SELF_SCOPED, &document)]);
let input = format!(r#"{{"@context": "{SELF_SCOPED}", "@id": "http://example.com/n", "t": {{"@id": "http://example.com/m", "t": "v"}}}}"#);
let properties = expanded_properties(&input, &loader).await;
assert_eq!(properties, vec!["http://example.com/s#t".to_owned()]);
}
#[tokio::test]
async fn a_top_level_context_including_itself_overflows() {
const SELF_INCLUDING: Iri<&str> = iri!("http://example.com/loop.jsonld");
let document = format!(r#"{{"@context": ["{SELF_INCLUDING}", {{"t": "http://example.com/l#t"}}]}}"#);
let loader = CountingLoader::new(&[(SELF_INCLUDING, &document)]);
let input = format!(r#"{{"@context": "{SELF_INCLUDING}", "@id": "http://example.com/n", "t": "v"}}"#);
let (json, _) = jsonld::syntax::Value::parse_str(&input).unwrap();
let result = json.expand(&loader).await;
assert!(result.is_err(), "a self-including top-level context must end in a context overflow");
assert_eq!(loader.loads(), 1, "the repeated document is dereferenced once however deep the chain grows");
}