use crate::{Options, TypeLangValue};
use contextual::WithContext;
use jsonld_core::{
Container,
Context,
Indexed,
Nullable,
Object,
ProcessingMode,
Term,
Type,
Value,
context::{
CACHED_KEYWORDS,
CompactIriKeyRef,
KeywordAliases,
inverse::{LangSelection, Selection, TypeSelection},
},
object,
};
use jsonld_syntax::{Keyword, is_keyword, is_keyword_like};
use rdfx::vocabulary::Vocabulary;
use smallvec::SmallVec;
use std::{hash::Hash, sync::Arc};
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("IRI confused with prefix")]
pub struct IriConfusedWithPrefix;
fn candidate_beats(key: &str, suffix: &str, candidate_len: usize, current: &str) -> bool {
use std::cmp::Ordering;
match candidate_len.cmp(¤t.len()) {
Ordering::Less => true,
Ordering::Greater => false,
Ordering::Equal => {
let cand = key.bytes().chain(std::iter::once(b':')).chain(suffix.bytes());
cand.cmp(current.bytes()) == Ordering::Less
}
}
}
pub(crate) struct CompactIriMemo<'a, T> {
key: Option<MemoKey<'a, T>>,
result: Option<Arc<str>>,
}
struct MemoKey<'a, T> {
containers: SmallVec<[Container; 8]>,
selection: Selection<'a, T>,
}
impl<T> CompactIriMemo<'_, T> {
pub fn new() -> Self {
Self { key: None, result: None }
}
}
impl<'a, T: PartialEq> CompactIriMemo<'a, T> {
#[allow(clippy::option_option)]
fn get(&self, containers: &[Container], selection: &Selection<'a, T>) -> Option<Option<Arc<str>>> {
let key = self.key.as_ref()?;
(key.containers.as_slice() == containers && key.selection == *selection).then(|| self.result.clone())
}
fn store(&mut self, containers: SmallVec<[Container; 8]>, selection: Selection<'a, T>, result: Option<Arc<str>>) {
self.key = Some(MemoKey { containers, selection });
self.result = result;
}
}
pub(crate) fn compact_iri<N>(
vocabulary: &N,
active_context: &Context<N::Iri, N::BlankId>,
var: &Term<N::Iri, N::BlankId>,
vocab: bool,
reverse: bool,
options: Options,
) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
where
N: Vocabulary,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
{
let cache = active_context.compact_iri_cache();
if let Some(hit) = cache.get(&CompactIriKeyRef(var, vocab, reverse, options.processing_mode)) {
return Ok(hit);
}
let result = compact_iri_full::<N, Object<N::Iri, N::BlankId>>(vocabulary, active_context, var, None, vocab, reverse, options, None)?;
cache.insert((var.clone(), vocab, reverse, options.processing_mode), result.clone());
Ok(result)
}
pub(crate) fn keyword_alias<'a, N>(vocabulary: &N, active_context: &'a Context<N::Iri, N::BlankId>, options: Options, k: Keyword) -> &'a str
where
N: Vocabulary,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
{
let aliases = active_context.keyword_aliases_or_init(options.processing_mode, || {
let arr: [Box<str>; 13] = std::array::from_fn(|i| {
let kw = CACHED_KEYWORDS[i];
match compact_iri(vocabulary, active_context, &Term::Keyword(kw), true, false, options).ok().flatten() {
Some(arc) => Box::<str>::from(&*arc),
None => Box::<str>::from(kw.into_str()),
}
});
KeywordAliases::new(arr)
});
aliases.get(k)
}
pub(crate) fn compact_iri_with<N, O>(
vocabulary: &N,
active_context: &Context<N::Iri, N::BlankId>,
var: &Term<N::Iri, N::BlankId>,
value: &Indexed<O>,
vocab: bool,
reverse: bool,
options: Options,
) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
where
N: Vocabulary,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
O: object::Any<N::Iri, N::BlankId>,
{
compact_iri_full(vocabulary, active_context, var, Some(value), vocab, reverse, options, None)
}
pub(crate) fn compact_iri_with_memo<'a, N, O>(
vocabulary: &N,
active_context: &'a Context<N::Iri, N::BlankId>,
var: &Term<N::Iri, N::BlankId>,
value: &'a Indexed<O>,
vocab: bool,
reverse: bool,
options: Options,
memo: &mut CompactIriMemo<'a, N::Iri>,
) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
where
N: Vocabulary,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
O: object::Any<N::Iri, N::BlankId>,
{
compact_iri_full(vocabulary, active_context, var, Some(value), vocab, reverse, options, Some(memo))
}
pub(crate) fn compact_iri_full<'a, N, O>(
vocabulary: &N,
active_context: &'a Context<N::Iri, N::BlankId>,
var: &Term<N::Iri, N::BlankId>,
value: Option<&'a Indexed<O>>,
vocab: bool,
reverse: bool,
options: Options,
mut memo: Option<&mut CompactIriMemo<'a, N::Iri>>,
) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
where
N: Vocabulary,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
O: object::Any<N::Iri, N::BlankId>,
{
if var.is_null() {
return Ok(None);
}
if vocab && let Some(entry) = active_context.inverse().get(var) {
let mut containers: SmallVec<[Container; 8]> = SmallVec::new();
let mut type_lang_value = None;
if let Some(value) = value
&& value.index().is_some()
&& !value.is_graph()
{
containers.push(Container::Index);
containers.push(Container::IndexSet);
}
let mut has_index = false;
let mut is_simple_value = false;
if reverse {
type_lang_value = Some(TypeLangValue::Type(TypeSelection::Reverse));
containers.push(Container::Set);
} else {
let value_ref = value.map(|v| {
has_index = v.index().is_some();
v.inner().as_ref()
});
match value_ref {
Some(object::Ref::List(list)) => {
if !has_index {
containers.push(Container::List);
}
let mut common_type = None;
let mut common_lang_dir = None;
if list.is_empty() {
common_lang_dir = Some(Nullable::Some((active_context.default_language(), active_context.default_base_direction())));
} else {
for item in list {
let mut item_type = None;
let mut item_lang_dir = None;
let mut is_value = false;
match item.inner() {
Object::Value(value) => {
is_value = true;
match value {
Value::LangString(lang_str) => item_lang_dir = Some(Nullable::Some((lang_str.language(), lang_str.direction()))),
Value::Literal(_, Some(ty)) => item_type = Some(Type::Iri(ty.clone())),
Value::Literal(_, None) => item_lang_dir = Some(Nullable::Null),
Value::Json(_) => item_type = Some(Type::Json),
}
}
_ => item_type = Some(Type::Id),
}
if common_lang_dir.is_none() {
common_lang_dir = item_lang_dir;
} else if is_value && common_lang_dir != item_lang_dir {
common_lang_dir = Some(Nullable::Some((None, None)));
}
if common_type.is_none() {
common_type = Some(item_type);
} else if common_type.as_ref().is_some_and(|t| *t != item_type) {
common_type = Some(None);
}
if common_lang_dir == Some(Nullable::Some((None, None))) && common_type == Some(None) {
break;
}
}
}
let common_lang_dir = common_lang_dir.unwrap_or(Nullable::Some((None, None)));
let common_type = common_type.unwrap_or(None);
if let Some(common_type) = common_type {
type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(common_type)));
} else {
type_lang_value = Some(TypeLangValue::Lang(LangSelection::Lang(common_lang_dir)));
}
}
Some(object::Ref::Node(node)) if node.is_graph() => {
if has_index {
containers.push(Container::GraphIndex);
containers.push(Container::GraphIndexSet);
}
if node.id.is_some() {
containers.push(Container::GraphId);
containers.push(Container::GraphIdSet);
}
containers.push(Container::Graph);
containers.push(Container::GraphSet);
containers.push(Container::Set);
if !has_index {
containers.push(Container::GraphIndex);
containers.push(Container::GraphIndexSet);
}
if node.id.is_none() {
containers.push(Container::GraphId);
containers.push(Container::GraphIdSet);
}
containers.push(Container::Index);
containers.push(Container::IndexSet);
type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(Type::Id)));
}
Some(object::Ref::Value(v)) => {
if (v.direction().is_some() || v.language().is_some()) && !has_index {
type_lang_value = Some(TypeLangValue::Lang(LangSelection::Lang(Nullable::Some((v.language(), v.direction())))));
containers.push(Container::Language);
containers.push(Container::LanguageSet);
} else if let Some(ty) = v.typ() {
type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(ty.as_syntax_type().cloned())));
} else {
is_simple_value = v.direction().is_none() && v.language().is_none() && !has_index;
}
containers.push(Container::Set);
}
_ => {
type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(Type::Id)));
containers.push(Container::Id);
containers.push(Container::IdSet);
containers.push(Container::Type);
containers.push(Container::SetType);
containers.push(Container::Set);
}
}
}
containers.push(Container::None);
if options.processing_mode != ProcessingMode::JsonLd1_0 && !has_index {
containers.push(Container::Index);
containers.push(Container::IndexSet);
}
if options.processing_mode != ProcessingMode::JsonLd1_0 && is_simple_value {
containers.push(Container::Language);
containers.push(Container::LanguageSet);
}
let mut is_empty_list = false;
if let Some(value) = value
&& let object::Ref::List(list) = value.inner().as_ref()
&& list.is_empty()
{
is_empty_list = true;
}
let selection = if is_empty_list {
Selection::Any
} else {
match type_lang_value {
Some(TypeLangValue::Type(type_value)) => {
let mut selection: Vec<TypeSelection<N::Iri>> = Vec::new();
if type_value == TypeSelection::Reverse {
selection.push(TypeSelection::Reverse);
}
let mut has_id_type = false;
if let Some(value) = value
&& let Some(id) = value.id()
&& (type_value == TypeSelection::Type(Type::Id) || type_value == TypeSelection::Reverse)
{
has_id_type = true;
let mut vocab = false;
if let Some(compacted_iri) = compact_iri(vocabulary, active_context, &id.clone().into_term(), true, false, options)?
&& let Some(def) = active_context.get(&*compacted_iri)
&& let Some(iri_mapping) = def.value()
{
vocab = iri_mapping == id;
}
if vocab {
selection.push(TypeSelection::Type(Type::Vocab));
selection.push(TypeSelection::Type(Type::Id));
} else {
selection.push(TypeSelection::Type(Type::Id));
selection.push(TypeSelection::Type(Type::Vocab));
}
selection.push(TypeSelection::Type(Type::None));
}
if !has_id_type {
selection.push(type_value);
selection.push(TypeSelection::Type(Type::None));
}
selection.push(TypeSelection::Any);
Selection::Type(selection)
}
Some(TypeLangValue::Lang(lang_value)) => {
let mut selection = vec![lang_value, LangSelection::Lang(Nullable::Some((None, None))), LangSelection::Any];
if let LangSelection::Lang(Nullable::Some((Some(_), Some(dir)))) = lang_value {
selection.push(LangSelection::Lang(Nullable::Some((None, Some(dir)))));
}
Selection::Lang(selection)
}
None => Selection::Lang(vec![
LangSelection::Lang(Nullable::Null),
LangSelection::Lang(Nullable::Some((None, None))),
LangSelection::Any,
]),
}
};
if let Some(memo) = memo.as_deref_mut()
&& let Some(hit) = memo.get(&containers, &selection)
{
return Ok(hit);
}
let result = match entry.select(&containers, &selection) {
Some(term) => Some(Arc::from(term.as_str())),
None => compact_iri_fallback(vocabulary, active_context, var, value.is_none(), vocab)?,
};
if let Some(memo) = memo {
memo.store(containers, selection, result.clone());
}
return Ok(result);
}
compact_iri_fallback(vocabulary, active_context, var, value.is_none(), vocab)
}
fn compact_iri_fallback<N>(
vocabulary: &N,
active_context: &Context<N::Iri, N::BlankId>,
var: &Term<N::Iri, N::BlankId>,
no_value: bool,
vocab: bool,
) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
where
N: Vocabulary,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
{
if vocab {
if let Some(vocab_mapping) = active_context.vocabulary() {
if let Some(suffix) = var.with(vocabulary).as_str().strip_prefix(vocab_mapping.with(vocabulary).as_str())
&& !suffix.is_empty()
&& active_context.get(suffix).is_none()
{
return Ok(Some(Arc::from(suffix)));
}
}
}
let mut compact_iri: Option<String> = None;
let var_str = var.with(vocabulary).as_str();
let mut buf: SmallVec<[u8; 64]> = SmallVec::new();
for (key, iri_mapping) in active_context.prefix_terms() {
let Some(suffix) = var_str.strip_prefix((**iri_mapping).with(vocabulary).as_str()) else {
continue;
};
if suffix.is_empty() {
continue;
}
let key_str = key.as_str();
let candidate_len = key_str.len() + 1 + suffix.len();
if let Some(current) = compact_iri.as_deref()
&& !candidate_beats(key_str, suffix, candidate_len, current)
{
continue;
}
buf.clear();
buf.reserve(candidate_len);
buf.extend_from_slice(key_str.as_bytes());
buf.push(b':');
buf.extend_from_slice(suffix.as_bytes());
let candidate_str: &str = unsafe { std::str::from_utf8_unchecked(&buf) };
let candidate_def = active_context.get(candidate_str);
let definition_ok = match candidate_def {
None => true,
Some(def) => def.value() == Some(var) && no_value,
};
if definition_ok {
compact_iri = Some(candidate_str.to_owned());
}
}
if let Some(compact_iri) = compact_iri {
return Ok(Some(Arc::from(compact_iri)));
}
if let Some(iri) = var.as_iri()
&& let Some(iri) = vocabulary.iri(iri)
&& active_context.contains_term(iri.scheme())
{
return Err(IriConfusedWithPrefix);
}
if !vocab
&& let Some(base_iri) = active_context.base_iri()
&& let Some(iri) = var.as_iri()
{
let Some(iri) = vocabulary.iri(iri) else {
return Ok(None);
};
let Some(base) = vocabulary.iri(base_iri) else {
return Ok(None);
};
let rel = iri.relative_to(&base);
let s = rel.as_str();
let out = if s.is_empty() {
match base.as_str().rsplit('/').next() {
Some(segment) if !segment.is_empty() => segment.to_string(),
_ => "./".to_string(),
}
} else {
s.to_string()
};
return Ok(Some(Arc::from(disambiguate_keyword(out))));
}
Ok(Some(Arc::from(var.with(vocabulary).to_string())))
}
fn disambiguate_keyword(s: String) -> String {
if is_keyword_like(&s) && !is_keyword(&s) { "./".to_string() + &s } else { s }
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use jsonld_core::{Id, ValidId, context::NormalTermDefinition};
use rdfx::{BlankIdBuf, IriBuf, vocabulary::no_vocabulary};
#[test]
fn compact_iri_cache_distinguishes_processing_modes() {
let iri = IriBuf::new("http://example.com/t".to_string()).unwrap();
let term = Term::<IriBuf, BlankIdBuf>::Id(Id::Valid(ValidId::Iri(iri)));
let mut context: Context<IriBuf, BlankIdBuf> = Context::new(None);
context.set_normal(
"t".into(),
Some(NormalTermDefinition {
value: Some(Arc::new(term.clone())),
container: Container::Index,
..Default::default()
}),
);
let options_1_1 = Options {
processing_mode: ProcessingMode::JsonLd1_1,
..Default::default()
};
let options_1_0 = Options {
processing_mode: ProcessingMode::JsonLd1_0,
..Default::default()
};
let compacted_1_1 = compact_iri(no_vocabulary(), &context, &term, true, false, options_1_1).unwrap();
let compacted_1_0 = compact_iri(no_vocabulary(), &context, &term, true, false, options_1_0).unwrap();
assert_eq!(compacted_1_1.as_deref(), Some("t"));
assert_eq!(compacted_1_0.as_deref(), Some("http://example.com/t"));
}
#[test]
fn relativization_of_base_with_trailing_slash_is_not_empty() {
let base = IriBuf::new("http://example.com/dir/".to_string()).unwrap();
let iri = IriBuf::new("http://example.com/dir/".to_string()).unwrap();
let term = Term::<IriBuf, BlankIdBuf>::Id(Id::Valid(ValidId::Iri(iri)));
let mut context: Context<IriBuf, BlankIdBuf> = Context::new(None);
context.set_base_iri(Some(base));
let compacted = compact_iri(no_vocabulary(), &context, &term, false, false, Options::default()).unwrap();
assert_eq!(compacted.as_deref(), Some("./"));
}
}