use futures::future::{BoxFuture, FutureExt};
use hashbrown::HashSet;
use iref::Iri;
use locspan::{Location, MapLocErr, Meta};
use mime::Mime;
use mown::Mown;
use rdf_types::{vocabulary::Index, IriVocabulary, IriVocabularyMut};
use static_iref::iri;
use std::fmt;
pub mod fs;
pub mod none;
pub use fs::FsLoader;
pub use none::NoLoader;
#[cfg(feature = "reqwest")]
pub mod reqwest;
#[cfg(feature = "reqwest")]
pub use self::reqwest::ReqwestLoader;
pub type LoadingResult<I, M, O, E> = Result<RemoteDocument<I, M, O>, E>;
#[derive(Clone)]
pub enum RemoteDocumentReference<I = Index, M = Location<I>, T = json_syntax::Value<M>> {
Iri(I),
Loaded(RemoteDocument<I, M, T>),
}
impl<I, M> RemoteDocumentReference<I, M, json_syntax::Value<M>> {
pub fn iri(iri: I) -> Self {
Self::Iri(iri)
}
}
impl<I, M> RemoteDocumentReference<I, M, json_ld_syntax::context::Value<M>> {
pub fn context_iri(iri: I) -> Self {
Self::Iri(iri)
}
}
impl<I, M, T> RemoteDocumentReference<I, M, T> {
pub async fn load_with<L: Loader<I, M>>(
self,
vocabulary: &mut (impl Sync + Send + IriVocabularyMut<Iri = I>),
loader: &mut L,
) -> LoadingResult<I, M, T, L::Error>
where
L::Output: Into<T>,
{
match self {
Self::Iri(r) => Ok(loader
.load_with(vocabulary, r)
.await?
.map(|m| m.map(Into::into))),
Self::Loaded(doc) => Ok(doc),
}
}
pub async fn loaded_with<L: Loader<I, M>>(
&self,
vocabulary: &mut (impl Sync + Send + IriVocabularyMut<Iri = I>),
loader: &mut L,
) -> Result<Mown<'_, RemoteDocument<I, M, T>>, L::Error>
where
I: Clone,
L::Output: Into<T>,
{
match self {
Self::Iri(r) => Ok(Mown::Owned(
loader
.load_with(vocabulary, r.clone())
.await?
.map(|m| m.map(Into::into)),
)),
Self::Loaded(doc) => Ok(Mown::Borrowed(doc)),
}
}
pub async fn load_context_with<L: ContextLoader<I, M>>(
self,
vocabulary: &mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
loader: &mut L,
) -> LoadingResult<I, M, T, L::ContextError>
where
L::Context: Into<T>,
{
match self {
Self::Iri(r) => Ok(loader
.load_context_with(vocabulary, r)
.await?
.map(|m| m.map(Into::into))),
Self::Loaded(doc) => Ok(doc),
}
}
pub async fn loaded_context_with<L: ContextLoader<I, M>>(
&self,
vocabulary: &mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
loader: &mut L,
) -> Result<Mown<'_, RemoteDocument<I, M, T>>, L::ContextError>
where
I: Clone,
L::Context: Into<T>,
{
match self {
Self::Iri(r) => Ok(Mown::Owned(
loader
.load_context_with(vocabulary, r.clone())
.await?
.map(|m| m.map(Into::into)),
)),
Self::Loaded(doc) => Ok(Mown::Borrowed(doc)),
}
}
}
#[derive(Clone)]
pub struct RemoteDocument<I = Index, M = Location<I>, T = json_syntax::Value<M>> {
url: Option<I>,
content_type: Option<Mime>,
context_url: Option<I>,
profile: HashSet<Profile<I>>,
document: Meta<T, M>,
}
impl<I, M, T> RemoteDocument<I, M, T> {
pub fn new(url: Option<I>, content_type: Option<Mime>, document: Meta<T, M>) -> Self {
Self::new_full(url, content_type, None, HashSet::new(), document)
}
pub fn new_full(
url: Option<I>,
content_type: Option<Mime>,
context_url: Option<I>,
profile: HashSet<Profile<I>>,
document: Meta<T, M>,
) -> Self {
Self {
url,
content_type,
context_url,
profile,
document,
}
}
pub fn map<U, N>(self, f: impl Fn(Meta<T, M>) -> Meta<U, N>) -> RemoteDocument<I, N, U> {
RemoteDocument {
url: self.url,
content_type: self.content_type,
context_url: self.context_url,
profile: self.profile,
document: f(self.document),
}
}
pub fn try_map<U, N, E>(
self,
f: impl Fn(Meta<T, M>) -> Result<Meta<U, N>, E>,
) -> Result<RemoteDocument<I, N, U>, E> {
Ok(RemoteDocument {
url: self.url,
content_type: self.content_type,
context_url: self.context_url,
profile: self.profile,
document: f(self.document)?,
})
}
pub fn url(&self) -> Option<&I> {
self.url.as_ref()
}
pub fn content_type(&self) -> Option<&Mime> {
self.content_type.as_ref()
}
pub fn context_url(&self) -> Option<&I> {
self.context_url.as_ref()
}
pub fn document(&self) -> &Meta<T, M> {
&self.document
}
pub fn into_document(self) -> Meta<T, M> {
self.document
}
pub fn into_url(self) -> Option<I> {
self.url
}
pub fn set_url(&mut self, url: Option<I>) {
self.url = url
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum StandardProfile {
Expanded,
Compacted,
Context,
Flattened,
Framed,
}
impl StandardProfile {
pub fn from_iri(iri: Iri) -> Option<Self> {
if iri == iri!("http://www.w3.org/ns/json-ld#expanded") {
Some(Self::Expanded)
} else if iri == iri!("http://www.w3.org/ns/json-ld#compacted") {
Some(Self::Compacted)
} else if iri == iri!("http://www.w3.org/ns/json-ld#context") {
Some(Self::Context)
} else if iri == iri!("http://www.w3.org/ns/json-ld#flattened") {
Some(Self::Flattened)
} else if iri == iri!("http://www.w3.org/ns/json-ld#framed") {
Some(Self::Framed)
} else {
None
}
}
pub fn iri(&self) -> Iri<'static> {
match self {
Self::Expanded => iri!("http://www.w3.org/ns/json-ld#expanded"),
Self::Compacted => iri!("http://www.w3.org/ns/json-ld#compacted"),
Self::Context => iri!("http://www.w3.org/ns/json-ld#context"),
Self::Flattened => iri!("http://www.w3.org/ns/json-ld#flattened"),
Self::Framed => iri!("http://www.w3.org/ns/json-ld#framed"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Profile<I> {
Standard(StandardProfile),
Custom(I),
}
impl<I> Profile<I> {
pub fn new(iri: Iri, vocabulary: &mut impl IriVocabularyMut<Iri = I>) -> Self {
match StandardProfile::from_iri(iri) {
Some(p) => Self::Standard(p),
None => Self::Custom(vocabulary.insert(iri)),
}
}
pub fn iri<'a>(&'a self, vocabulary: &'a impl IriVocabulary<Iri = I>) -> Iri<'a> {
match self {
Self::Standard(s) => s.iri(),
Self::Custom(c) => vocabulary.iri(c).unwrap(),
}
}
}
pub trait Loader<I, M> {
type Output;
type Error;
fn load_with<'a>(
&'a mut self,
vocabulary: &'a mut (impl Sync + Send + IriVocabularyMut<Iri = I>),
url: I,
) -> BoxFuture<'a, LoadingResult<I, M, Self::Output, Self::Error>>
where
I: 'a;
fn load<'a>(
&'a mut self,
url: I,
) -> BoxFuture<'a, LoadingResult<I, M, Self::Output, Self::Error>>
where
I: 'a,
(): IriVocabulary<Iri = I>,
{
self.load_with(rdf_types::vocabulary::no_vocabulary_mut(), url)
}
}
pub trait ContextLoader<I, M> {
type Context;
type ContextError;
fn load_context_with<'a>(
&'a mut self,
vocabulary: &'a mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
url: I,
) -> BoxFuture<'a, LoadingResult<I, M, Self::Context, Self::ContextError>>
where
I: 'a,
M: 'a;
fn load_context<'a>(
&'a mut self,
url: I,
) -> BoxFuture<'a, LoadingResult<I, M, Self::Context, Self::ContextError>>
where
I: 'a,
M: 'a,
(): IriVocabulary<Iri = I>,
{
self.load_context_with(rdf_types::vocabulary::no_vocabulary_mut(), url)
}
}
pub trait ExtractContext<M>: Sized {
type Context;
type Error;
fn extract_context(value: Meta<Self, M>) -> Result<Meta<Self::Context, M>, Self::Error>;
}
#[derive(Debug)]
pub enum ExtractContextError<M> {
Unexpected(json_syntax::Kind),
NoContext,
DuplicateContext(M),
Syntax(json_ld_syntax::context::InvalidContext),
}
impl<M> ExtractContextError<M> {
fn duplicate_context(
json_syntax::object::Duplicate(a, b): json_syntax::object::Duplicate<
json_syntax::object::Entry<M>,
>,
) -> Meta<Self, M> {
Meta(
Self::DuplicateContext(a.key.into_metadata()),
b.key.into_metadata(),
)
}
}
impl<M> fmt::Display for ExtractContextError<M> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unexpected(k) => write!(f, "unexpected {}", k),
Self::NoContext => write!(f, "missing context"),
Self::DuplicateContext(_) => write!(f, "duplicate context"),
Self::Syntax(e) => e.fmt(f),
}
}
}
impl<M: Clone> ExtractContext<M> for json_syntax::Value<M> {
type Context = json_ld_syntax::context::Value<M>;
type Error = Meta<ExtractContextError<M>, M>;
fn extract_context(
Meta(value, meta): Meta<Self, M>,
) -> Result<Meta<Self::Context, M>, Self::Error> {
match value {
json_syntax::Value::Object(mut o) => match o
.remove_unique("@context")
.map_err(ExtractContextError::duplicate_context)?
{
Some(context) => {
use json_ld_syntax::TryFromJson;
json_ld_syntax::context::Value::try_from_json(context.value)
.map_loc_err(ExtractContextError::Syntax)
}
None => Err(Meta(ExtractContextError::NoContext, meta)),
},
other => Err(Meta(ExtractContextError::Unexpected(other.kind()), meta)),
}
}
}
#[derive(Debug)]
pub enum ContextLoaderError<D, C> {
LoadingDocumentFailed(D),
ContextExtractionFailed(C),
}
impl<D: fmt::Display, C: fmt::Display> fmt::Display for ContextLoaderError<D, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::LoadingDocumentFailed(e) => e.fmt(f),
Self::ContextExtractionFailed(e) => e.fmt(f),
}
}
}
impl<I: Send, M, L: Loader<I, M>> ContextLoader<I, M> for L
where
L::Output: ExtractContext<M>,
{
type Context = <L::Output as ExtractContext<M>>::Context;
type ContextError = ContextLoaderError<L::Error, <L::Output as ExtractContext<M>>::Error>;
fn load_context_with<'a>(
&'a mut self,
vocabulary: &'a mut (impl Send + Sync + IriVocabularyMut<Iri = I>),
url: I,
) -> BoxFuture<'a, Result<RemoteDocument<I, M, Self::Context>, Self::ContextError>>
where
I: 'a,
M: 'a,
{
let load_document = self.load_with(vocabulary, url);
async move {
let doc = load_document
.await
.map_err(ContextLoaderError::LoadingDocumentFailed)?;
doc.try_map(ExtractContext::extract_context)
.map_err(ContextLoaderError::ContextExtractionFailed)
}
.boxed()
}
}