use std::{hash::Hash, sync::Arc};
use super::{DefinedTerms, Environment, Merged};
use crate::{Error, Options, ProcessingStack, Warning, WarningHandler};
use contextual::WithContext;
use iri_rs::{Iri, IriRef};
use jsonld_core::{Context, Id, Loader, NoLoader, ProcessingMode, Term, warning};
use jsonld_syntax::{self as syntax, ExpandableRef, Nullable, context::definition::Key, native_stack::Recursion};
use rdfx::{
BlankId,
vocabulary::{BlankIdVocabulary, IriVocabulary, Vocabulary, VocabularyMut},
};
use syntax::{CompactIri, context::definition::KeyOrKeywordRef, is_keyword_like};
pub struct MalformedIri(pub String);
impl From<MalformedIri> for Warning {
fn from(MalformedIri(s): MalformedIri) -> Self {
Self::MalformedIri(s)
}
}
pub type ExpandIriResult<T, B, E> = Result<Option<Arc<Term<T, B>>>, Error<E>>;
pub async fn expand_iri_with<'a, N, L, W>(
mut env: Environment<'a, N, L, W>,
active_context: &'a mut Context<N::Iri, N::BlankId>,
value: Nullable<ExpandableRef<'a>>,
document_relative: bool,
vocab: Option<Action>,
local_context: &'a Merged<'a>,
defined: &'a mut DefinedTerms,
remote_contexts: ProcessingStack<N::Iri>,
options: Options,
) -> ExpandIriResult<N::Iri, N::BlankId, L::Error>
where
N: VocabularyMut,
N::Iri: Clone + Eq + Hash,
N::BlankId: Clone + PartialEq,
L: Loader,
W: WarningHandler<N>,
{
match value {
Nullable::Null => Ok(Some(Arc::new(Term::Null))),
Nullable::Some(ExpandableRef::Keyword(k)) => Ok(Some(Arc::new(Term::Keyword(k)))),
Nullable::Some(ExpandableRef::String(value)) => {
if is_keyword_like(value) {
return Ok(Some(Arc::new(Term::Null)));
}
Recursion::new(super::define(
Environment {
vocabulary: env.vocabulary,
loader: env.loader,
warnings: env.warnings,
},
active_context,
local_context,
value.into(),
defined,
remote_contexts.clone(),
None,
false,
options.with_no_override(),
))
.await?;
if let Some(term_definition) = active_context.get(value) {
if let Some(arc) = term_definition.value_arc()
&& arc.is_keyword()
{
return Ok(Some(Arc::clone(arc)));
}
if vocab.is_some() {
return match term_definition.value_arc() {
Some(arc) => Ok(Some(Arc::clone(arc))),
None => Ok(Some(Arc::new(Term::Null))),
};
}
}
if value.find(':').is_some_and(|i| i > 0) {
if let Ok(blank_id) = BlankId::new(value) {
return Ok(Some(Arc::new(Term::Id(Id::blank(env.vocabulary.insert_blank_id(blank_id))))));
}
if value == "_:" {
return Ok(Some(Arc::new(Term::Id(Id::Invalid("_:".to_string())))));
}
if let Ok(compact_iri) = CompactIri::new(value) {
Recursion::new(super::define(
Environment {
vocabulary: env.vocabulary,
loader: env.loader,
warnings: env.warnings,
},
active_context,
local_context,
KeyOrKeywordRef::Key(compact_iri.prefix().into()),
defined,
remote_contexts,
None,
false,
options.with_no_override(),
))
.await?;
let prefix_key = Key::from(compact_iri.prefix());
if let Some(term_definition) = active_context.get_normal(&prefix_key)
&& (term_definition.prefix || active_context.processing_mode() == ProcessingMode::JsonLd1_0)
&& let Some(mapping) = term_definition.value()
{
let mut result = mapping.with(&*env.vocabulary).as_str().to_string();
result.push_str(compact_iri.suffix());
return Ok(Some(Arc::new(Term::Id(Id::from_string_in(env.vocabulary, result)))));
}
}
if let Ok(iri) = Iri::parse(value) {
return Ok(Some(Arc::new(Term::Id(Id::iri(env.vocabulary.insert(iri))))));
}
}
if let Some(action) = vocab {
match active_context.vocabulary() {
Some(Term::Id(mapping)) => {
return match action {
Action::Keep => {
let mut result = mapping.with(&*env.vocabulary).as_str().to_string();
result.push_str(value);
Ok(Some(Arc::new(Term::Id(Id::from_string_in(env.vocabulary, result)))))
}
Action::Drop => Ok(None),
Action::Reject => Err(Error::ForbiddenVocab),
};
}
Some(_) => return Ok(Some(Arc::new(invalid_iri(&mut env, value.to_string())))),
None => (),
}
}
if document_relative
&& let Ok(iri_ref) = IriRef::parse(value)
&& let Some(iri) = super::resolve_iri(env.vocabulary, iri_ref, active_context.base_iri())
{
return Ok(Some(Arc::new(Term::from(iri))));
}
Ok(Some(Arc::new(invalid_iri(&mut env, value.to_string()))))
}
}
}
fn invalid_iri<N, L, W: jsonld_core::warning::Handler<N, Warning>>(env: &mut Environment<N, L, W>, value: String) -> Term<N::Iri, N::BlankId>
where
N: Vocabulary,
{
env.warnings.handle(env.vocabulary, MalformedIri(value.clone()).into());
Term::Id(Id::Invalid(value))
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Action {
#[default]
Keep,
Drop,
Reject,
}
impl Action {
#[must_use]
pub fn is_reject(&self) -> bool {
matches!(self, Self::Reject)
}
}
#[derive(Debug)]
pub struct RejectVocab;
pub type IriExpansionResult<N> = Result<Option<Arc<Term<<N as IriVocabulary>::Iri, <N as BlankIdVocabulary>::BlankId>>>, RejectVocab>;
#[allow(clippy::needless_pass_by_value)]
pub fn expand_iri_simple<W, N, L, H>(
env: &mut Environment<N, L, H>,
active_context: &Context<N::Iri, N::BlankId>,
value: Nullable<ExpandableRef>,
document_relative: bool,
vocab: Option<Action>,
) -> IriExpansionResult<N>
where
N: VocabularyMut,
N::Iri: Clone,
N::BlankId: Clone,
W: From<MalformedIri>,
H: warning::Handler<N, W>,
{
let cache_value: Option<&str> = if !document_relative && matches!(vocab, Some(Action::Keep)) {
if let Nullable::Some(ExpandableRef::String(s)) = &value {
Some(*s)
} else {
None
}
} else {
None
};
if let Some(s) = cache_value
&& let Some(arc) = active_context.term_resolution_cache().get(s)
{
return Ok(Some(arc));
}
let result = expand_iri_simple_inner::<W, N, L, H>(env, active_context, value, document_relative, vocab)?;
if let (Some(s), Some(arc)) = (cache_value, &result) {
active_context.term_resolution_cache().insert(Box::from(s), Arc::clone(arc));
}
Ok(result)
}
#[allow(clippy::needless_pass_by_value)]
fn expand_iri_simple_inner<W, N, L, H>(
env: &mut Environment<N, L, H>,
active_context: &Context<N::Iri, N::BlankId>,
value: Nullable<ExpandableRef>,
document_relative: bool,
vocab: Option<Action>,
) -> IriExpansionResult<N>
where
N: VocabularyMut,
N::Iri: Clone,
N::BlankId: Clone,
W: From<MalformedIri>,
H: warning::Handler<N, W>,
{
match value {
Nullable::Null => Ok(Some(Arc::new(Term::Null))),
Nullable::Some(ExpandableRef::Keyword(k)) => Ok(Some(Arc::new(Term::Keyword(k)))),
Nullable::Some(ExpandableRef::String(value)) => {
if is_keyword_like(value) {
return Ok(Some(Arc::new(Term::Null)));
}
if let Some(term_definition) = active_context.get(value) {
if let Some(arc) = term_definition.value_arc()
&& arc.is_keyword()
{
return Ok(Some(Arc::clone(arc)));
}
if vocab.is_some() {
return match term_definition.value_arc() {
Some(arc) => Ok(Some(Arc::clone(arc))),
None => Ok(Some(Arc::new(Term::Null))),
};
}
}
if value.find(':').is_some_and(|i| i > 0) {
if let Ok(blank_id) = BlankId::new(value) {
return Ok(Some(Arc::new(Term::Id(Id::blank(env.vocabulary.insert_blank_id(blank_id))))));
}
if value == "_:" {
return Ok(Some(Arc::new(Term::Id(Id::Invalid("_:".to_string())))));
}
if let Ok(compact_iri) = CompactIri::new(value) {
let prefix_key = Key::from(compact_iri.prefix());
if let Some(term_definition) = active_context.get_normal(&prefix_key)
&& (term_definition.prefix || active_context.processing_mode() == ProcessingMode::JsonLd1_0)
&& let Some(mapping) = term_definition.value()
{
let mut result = mapping.with(&*env.vocabulary).as_str().to_string();
result.push_str(compact_iri.suffix());
return Ok(Some(Arc::new(Term::Id(Id::from_string_in(env.vocabulary, result)))));
}
}
if let Ok(iri) = Iri::parse(value) {
return Ok(Some(Arc::new(Term::Id(Id::iri(env.vocabulary.insert(iri))))));
}
}
if let Some(action) = vocab {
match active_context.vocabulary() {
Some(Term::Id(mapping)) => {
return match action {
Action::Keep => {
let mut result = mapping.with(&*env.vocabulary).as_str().to_string();
result.push_str(value);
Ok(Some(Arc::new(Term::Id(Id::from_string_in(env.vocabulary, result)))))
}
Action::Drop => Ok(None),
Action::Reject => Err(RejectVocab),
};
}
Some(_) => return Ok(Some(Arc::new(invalid_iri_simple(env, value.to_string())))),
None => (),
}
}
if document_relative
&& let Ok(iri_ref) = IriRef::parse(value)
&& let Some(iri) = super::resolve_iri(env.vocabulary, iri_ref, active_context.base_iri())
{
return Ok(Some(Arc::new(Term::from(iri))));
}
Ok(Some(Arc::new(invalid_iri_simple(env, value.to_string()))))
}
}
}
fn invalid_iri_simple<W, N, L, H>(env: &mut Environment<N, L, H>, value: String) -> Term<N::Iri, N::BlankId>
where
N: Vocabulary,
W: From<MalformedIri>,
H: warning::Handler<N, W>,
{
env.warnings.handle(env.vocabulary, MalformedIri(value.clone()).into());
Term::Id(Id::Invalid(value))
}
pub fn expand_property_key<N: VocabularyMut>(
vocabulary: &mut N,
active_context: &Context<N::Iri, N::BlankId>,
term: &str,
policy: Action,
) -> IriExpansionResult<N>
where
N::Iri: Clone,
N::BlankId: Clone,
{
expand_iri_simple::<MalformedIri, N, NoLoader, ()>(
&mut Environment {
vocabulary,
loader: &NoLoader,
warnings: &mut (),
},
active_context,
Nullable::Some(term.into()),
false,
Some(policy),
)
}
pub fn expand_type_value<N: VocabularyMut>(
vocabulary: &mut N,
active_context: &Context<N::Iri, N::BlankId>,
value: &str,
policy: Action,
) -> IriExpansionResult<N>
where
N::Iri: Clone,
N::BlankId: Clone,
{
expand_iri_simple::<MalformedIri, N, NoLoader, ()>(
&mut Environment {
vocabulary,
loader: &NoLoader,
warnings: &mut (),
},
active_context,
Nullable::Some(value.into()),
true,
Some(policy),
)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::{Options, algorithm::sync::process_context_sync};
use iri_rs::IriBuf;
use jsonld_core::NoLoader;
use jsonld_syntax::{Keyword, Parse, TryFromJson};
use rdfx::{BlankIdBuf, vocabulary::no_vocabulary_mut};
const CONTEXT: &str = r#"{
"@vocab": "https://vocab.example/",
"ex": "https://example.com/",
"name": "https://example.com/name",
"type": "@type"
}"#;
fn processed_context() -> Context<IriBuf, BlankIdBuf> {
let (json, _) = jsonld_syntax::Value::parse_str(CONTEXT).unwrap();
let local = syntax::context::Context::try_from_json(&json).unwrap();
let active = Context::new(None);
let mut warnings = ();
process_context_sync(
Environment {
vocabulary: no_vocabulary_mut(),
loader: &NoLoader,
warnings: &mut warnings,
},
&active,
&local,
ProcessingStack::default(),
None,
Options::default(),
0,
)
.unwrap()
.into_processed()
}
fn expand(context: &Context<IriBuf, BlankIdBuf>, value: &str, vocab: bool) -> Term<IriBuf, BlankIdBuf> {
let mut warnings = ();
let mut env = Environment {
vocabulary: no_vocabulary_mut(),
loader: &NoLoader,
warnings: &mut warnings,
};
let expanded = expand_iri_simple::<Warning, _, _, _>(
&mut env,
context,
Nullable::Some(value.into()),
false,
vocab.then_some(Action::Keep),
)
.unwrap()
.unwrap();
Arc::unwrap_or_clone(expanded)
}
fn iri(source: &str) -> Term<IriBuf, BlankIdBuf> {
Term::Id(Id::iri(IriBuf::new(source.to_owned()).unwrap()))
}
#[test]
fn processed_context_expands_terms_compact_iris_and_vocab_relative_terms() {
let context = processed_context();
let cases = [
("name", true, iri("https://example.com/name")),
("name", false, Term::Id(Id::Invalid("name".to_owned()))),
("ex:thing", true, iri("https://example.com/thing")),
("ex:thing", false, iri("https://example.com/thing")),
("http://other.example/x", true, iri("http://other.example/x")),
("http://other.example/x", false, iri("http://other.example/x")),
("temperature", true, iri("https://vocab.example/temperature")),
("temperature", false, Term::Id(Id::Invalid("temperature".to_owned()))),
("type", true, Term::Keyword(Keyword::Type)),
("_:b0", false, Term::Id(Id::blank(BlankIdBuf::new("_:b0".to_owned()).unwrap()))),
];
for (value, vocab, expected) in cases {
assert_eq!(expand(&context, value, vocab), expected, "{value} (vocab: {vocab})");
}
}
}