use std::{borrow::Cow, hash::Hash};
use json_ld::expansion::Action;
use linked_data::{LinkedData, LinkedDataResource, LinkedDataSubject};
pub use json_ld;
pub use json_ld::*;
use serde::{Deserialize, Serialize};
use ssi_rdf::{
generator, interpretation::WithGenerator, Interpretation, LdEnvironment, Vocabulary,
VocabularyMut,
};
mod contexts;
pub use contexts::*;
pub trait JsonLdLoaderProvider {
type Loader: json_ld::Loader;
fn loader(&self) -> &Self::Loader;
}
impl<E: JsonLdLoaderProvider> JsonLdLoaderProvider for &E {
type Loader = E::Loader;
fn loader(&self) -> &Self::Loader {
E::loader(*self)
}
}
#[derive(Debug, thiserror::Error)]
pub enum JsonLdError {
#[error("expansion error: {0}")]
Expansion(#[from] json_ld::expansion::Error),
#[error("interpretation error: {0}")]
Interpretation(#[from] linked_data::IntoQuadsError),
}
#[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CompactJsonLd(pub json_syntax::Value);
impl CompactJsonLd {
pub fn from_value_ref(value: &json_syntax::Value) -> &Self {
unsafe { std::mem::transmute(value) }
}
}
pub trait Expandable: Sized {
type Error: std::fmt::Display;
type Expanded<I: Interpretation, V: Vocabulary>: LinkedData<I, V>
where
I: Interpretation,
V: VocabularyMut,
V::Iri: LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
V::BlankId: LinkedDataResource<I, V> + LinkedDataSubject<I, V>;
#[allow(async_fn_in_trait)]
async fn expand_with<I, V>(
&self,
ld: &mut LdEnvironment<V, I>,
loader: &impl Loader,
) -> Result<Self::Expanded<I, V>, Self::Error>
where
I: Interpretation,
V: VocabularyMut,
V::Iri: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
V::BlankId: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>;
#[allow(async_fn_in_trait)]
async fn expand(
&self,
loader: &impl Loader,
) -> Result<Self::Expanded<WithGenerator<generator::Blank>, ()>, Self::Error> {
let mut ld = LdEnvironment::default();
self.expand_with(&mut ld, loader).await
}
}
impl Expandable for CompactJsonLd {
type Error = JsonLdError;
type Expanded<I, V>
= json_ld::ExpandedDocument<V::Iri, V::BlankId>
where
I: Interpretation,
V: VocabularyMut,
V::Iri: LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
V::BlankId: LinkedDataResource<I, V> + LinkedDataSubject<I, V>;
async fn expand_with<I, V>(
&self,
ld: &mut LdEnvironment<V, I>,
loader: &impl Loader,
) -> Result<Self::Expanded<I, V>, Self::Error>
where
I: Interpretation,
V: VocabularyMut,
V::Iri: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
V::BlankId: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
{
let expanded = self
.0
.expand_full(
&mut ld.vocabulary,
Default::default(),
None,
loader,
json_ld::expansion::Options {
policy: json_ld::expansion::Policy {
invalid: Action::Reject,
allow_undefined: false,
..Default::default()
},
..Default::default()
},
(),
)
.await?;
Ok(expanded)
}
}
pub trait JsonLdObject {
fn json_ld_context(&'_ self) -> Option<Cow<'_, json_ld::syntax::Context>> {
None
}
}
impl JsonLdObject for CompactJsonLd {
fn json_ld_context(&'_ self) -> Option<Cow<'_, json_ld::syntax::Context>> {
json_syntax::from_value(self.0.as_object()?.get("@context").next()?.clone())
.map(Cow::Owned)
.ok()
}
}
pub trait JsonLdNodeObject: JsonLdObject {
fn json_ld_type(&'_ self) -> JsonLdTypes<'_> {
JsonLdTypes::default()
}
}
#[derive(Debug)]
pub struct JsonLdTypes<'a> {
static_: &'static [&'static str],
non_static: Cow<'a, [String]>,
}
impl Default for JsonLdTypes<'_> {
fn default() -> Self {
Self::new(&[], Cow::Owned(vec![]))
}
}
impl<'a> JsonLdTypes<'a> {
pub fn new(static_: &'static [&'static str], non_static: Cow<'a, [String]>) -> Self {
Self {
static_,
non_static,
}
}
pub fn len(&self) -> usize {
self.static_.len() + self.non_static.len()
}
pub fn is_empty(&self) -> bool {
self.static_.is_empty() && self.non_static.is_empty()
}
pub fn reborrow(&'_ self) -> JsonLdTypes<'_> {
JsonLdTypes {
static_: self.static_,
non_static: Cow::Borrowed(&self.non_static),
}
}
}
impl From<&'static &'static str> for JsonLdTypes<'_> {
fn from(value: &'static &'static str) -> Self {
Self::new(std::slice::from_ref(value), Cow::Owned(vec![]))
}
}
impl<'a> From<&'a [String]> for JsonLdTypes<'a> {
fn from(value: &'a [String]) -> Self {
Self::new(&[], Cow::Borrowed(value))
}
}
impl From<Vec<String>> for JsonLdTypes<'_> {
fn from(value: Vec<String>) -> Self {
Self::new(&[], Cow::Owned(value))
}
}
impl<'a> From<Cow<'a, [String]>> for JsonLdTypes<'a> {
fn from(value: Cow<'a, [String]>) -> Self {
Self::new(&[], value)
}
}
impl serde::Serialize for JsonLdTypes<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(Some(self.static_.len() + self.non_static.len()))?;
for ty in self.static_ {
seq.serialize_element(ty)?;
}
for ty in self.non_static.as_ref() {
seq.serialize_element(ty)?;
}
seq.end()
}
}
impl<'a> From<JsonLdTypes<'a>> for Vec<String> {
fn from(value: JsonLdTypes<'a>) -> Self {
let mut result = Vec::with_capacity(value.len());
result.extend(value.static_.iter().map(|s| s.to_string()));
result.extend(value.non_static.into_owned());
result
}
}
pub struct WithContext<T> {
pub context: Option<json_ld::syntax::Context>,
pub value: T,
}
impl<T> WithContext<T> {
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> WithContext<U> {
WithContext {
context: self.context,
value: f(self.value),
}
}
}
#[cfg(test)]
mod tests {
use crate::{CompactJsonLd, ContextLoader, Expandable};
#[tokio::test]
async fn accept_defined_type() {
let input = CompactJsonLd(json_syntax::json!({
"@context": { "Defined": "http://example.org/#Defined" },
"@type": ["Defined"]
}));
assert!(input.expand(&ContextLoader::default()).await.is_ok());
}
#[tokio::test]
async fn reject_undefined_type() {
let input = CompactJsonLd(json_syntax::json!({
"@type": ["Undefined"]
}));
assert!(input.expand(&ContextLoader::default()).await.is_err());
}
#[tokio::test]
async fn accept_defined_property() {
let input = CompactJsonLd(json_syntax::json!({
"@context": { "defined": "http://example.org/#defined" },
"defined": "foo"
}));
assert!(input.expand(&ContextLoader::default()).await.is_ok());
}
#[tokio::test]
async fn reject_undefined_property() {
let input = CompactJsonLd(json_syntax::json!({
"undefined": "foo"
}));
assert!(input.expand(&ContextLoader::default()).await.is_err());
}
}