use crate::{
Options,
TypeLangValue,
value_hint::{IntoValueFeatures, ValueFeatures, ValueHint, ValueShape},
};
use contextual::WithContext;
use jsonld_core::{
Container,
Context,
Indexed,
Nullable,
ProcessingMode,
Term,
Type,
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 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, ValueHint<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 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 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 fn compact_iri_with_hint<'a, N>(
vocabulary: &N,
active_context: &'a Context<N::Iri, N::BlankId>,
var: &Term<N::Iri, N::BlankId>,
hint: ValueHint<'a, 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,
{
compact_iri_full(vocabulary, active_context, var, Some(hint), 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, V>(
vocabulary: &N,
active_context: &'a Context<N::Iri, N::BlankId>,
var: &Term<N::Iri, N::BlankId>,
value: Option<V>,
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,
V: IntoValueFeatures<'a, N::Iri, N::BlankId>,
{
if var.is_null() {
return Ok(None);
}
let has_value = value.is_some();
if vocab && let Some(entry) = active_context.inverse().get(var) {
let value = value.map(|value| value.into_value_features(active_context));
let mut containers: SmallVec<[Container; 8]> = SmallVec::new();
let mut type_lang_value = None;
if let Some(value) = &value
&& value.has_index
&& !value.is_graph()
{
containers.push(Container::Index);
containers.push(Container::IndexSet);
}
let mut has_index = false;
let mut is_simple_value = false; let value_id = value.as_ref().and_then(ValueFeatures::id);
let is_empty_list = value.as_ref().is_some_and(ValueFeatures::is_empty_list);
if reverse {
type_lang_value = Some(TypeLangValue::Type(TypeSelection::Reverse));
containers.push(Container::Set);
} else {
let shape = value.map(|v| {
has_index = v.has_index;
v.shape
});
match shape {
Some(ValueShape::List {
common_type, common_lang_dir, ..
}) => {
if !has_index {
containers.push(Container::List);
}
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(ValueShape::Graph { id }) => {
if has_index {
containers.push(Container::GraphIndex);
containers.push(Container::GraphIndexSet);
}
if 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 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(ValueShape::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.cloned())));
} else {
is_simple_value = v.direction().is_none() && v.language().is_none() && !has_index;
}
containers.push(Container::Set);
}
Some(ValueShape::Node { .. }) | None => {
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 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(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, !has_value, vocab)?,
};
if let Some(memo) = memo {
memo.store(containers, selection, result.clone());
}
return Ok(result);
}
compact_iri_fallback(vocabulary, active_context, var, !has_value, 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("./"));
}
#[tokio::test]
async fn keyword_alias_returns_the_alias_or_the_keyword() {
let context = crate::test_fixtures::processed_context(
r#"{ "id": "@id", "type": "@type", "value": "@value", "list": "@list", "lang": "@language", "json": "@json" }"#,
)
.await;
let cases = [
(Keyword::Id, "id"),
(Keyword::Type, "type"),
(Keyword::Value, "value"),
(Keyword::List, "list"),
(Keyword::Language, "lang"),
(Keyword::Json, "json"),
(Keyword::Direction, "@direction"),
(Keyword::Index, "@index"),
];
for (keyword, alias) in cases {
assert_eq!(keyword_alias(no_vocabulary(), &context, Options::default(), keyword), alias, "{keyword:?}");
}
}
}