use serde_json::{Map, Value};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum JsonLdError {
#[error("Invalid context: {0}")]
InvalidContext(String),
#[error("Invalid IRI: {0}")]
InvalidIri(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Missing key: {0}")]
MissingKey(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Framing error: {0}")]
Framing(String),
}
pub type JsonLdResult<T> = Result<T, JsonLdError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JsonLdTerm {
Iri(String),
BlankNode(String),
Literal {
value: String,
datatype: String,
language: Option<String>,
},
}
impl JsonLdTerm {
pub fn is_iri(&self) -> bool {
matches!(self, Self::Iri(_))
}
pub fn is_blank_node(&self) -> bool {
matches!(self, Self::BlankNode(_))
}
pub fn is_literal(&self) -> bool {
matches!(self, Self::Literal { .. })
}
pub fn to_nquads_string(&self) -> String {
match self {
Self::Iri(iri) => format!("<{}>", iri),
Self::BlankNode(id) => id.clone(),
Self::Literal {
value,
datatype,
language,
} => {
let escaped = value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
if let Some(lang) = language {
format!("\"{}\"@{}", escaped, lang)
} else {
format!("\"{}\"^^<{}>", escaped, datatype)
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonLdQuad {
pub subject: JsonLdTerm,
pub predicate: JsonLdTerm,
pub object: JsonLdTerm,
pub graph: Option<JsonLdTerm>,
}
impl JsonLdQuad {
pub fn triple(subject: JsonLdTerm, predicate: JsonLdTerm, object: JsonLdTerm) -> Self {
Self {
subject,
predicate,
object,
graph: None,
}
}
pub fn named(
subject: JsonLdTerm,
predicate: JsonLdTerm,
object: JsonLdTerm,
graph: JsonLdTerm,
) -> Self {
Self {
subject,
predicate,
object,
graph: Some(graph),
}
}
pub fn to_nquads_line(&self) -> String {
if let Some(g) = &self.graph {
format!(
"{} {} {} {} .",
self.subject.to_nquads_string(),
self.predicate.to_nquads_string(),
self.object.to_nquads_string(),
g.to_nquads_string()
)
} else {
format!(
"{} {} {} .",
self.subject.to_nquads_string(),
self.predicate.to_nquads_string(),
self.object.to_nquads_string()
)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContainerType {
List,
Set,
Index,
Language,
Id,
Graph,
}
impl ContainerType {
pub(crate) fn from_str(s: &str) -> Option<Self> {
match s {
"@list" => Some(Self::List),
"@set" => Some(Self::Set),
"@index" => Some(Self::Index),
"@language" => Some(Self::Language),
"@id" => Some(Self::Id),
"@graph" => Some(Self::Graph),
_ => None,
}
}
#[allow(dead_code)]
pub(crate) fn as_str(&self) -> &'static str {
match self {
Self::List => "@list",
Self::Set => "@set",
Self::Index => "@index",
Self::Language => "@language",
Self::Id => "@id",
Self::Graph => "@graph",
}
}
}
#[derive(Debug, Clone)]
pub struct TermDefinition {
pub iri: String,
pub container: Option<ContainerType>,
pub language: Option<String>,
pub type_coercion: Option<String>,
}
pub(crate) fn builtin_prefixes() -> HashMap<String, String> {
let mut m = HashMap::new();
m.insert(
"rdf".into(),
"http://www.w3.org/1999/02/22-rdf-syntax-ns#".into(),
);
m.insert(
"rdfs".into(),
"http://www.w3.org/2000/01/rdf-schema#".into(),
);
m.insert("owl".into(), "http://www.w3.org/2002/07/owl#".into());
m.insert("xsd".into(), "http://www.w3.org/2001/XMLSchema#".into());
m.insert("schema".into(), "http://schema.org/".into());
m.insert("dc".into(), "http://purl.org/dc/elements/1.1/".into());
m.insert("dcterms".into(), "http://purl.org/dc/terms/".into());
m.insert("foaf".into(), "http://xmlns.com/foaf/0.1/".into());
m.insert("skos".into(), "http://www.w3.org/2004/02/skos/core#".into());
m
}
#[derive(Debug, Clone)]
pub struct JsonLdContext {
pub base_iri: Option<String>,
pub vocab: Option<String>,
pub prefixes: HashMap<String, String>,
pub terms: HashMap<String, TermDefinition>,
pub default_language: Option<String>,
}
impl Default for JsonLdContext {
fn default() -> Self {
Self {
base_iri: None,
vocab: None,
prefixes: builtin_prefixes(),
terms: HashMap::new(),
default_language: None,
}
}
}
impl JsonLdContext {
pub fn empty() -> Self {
Self {
base_iri: None,
vocab: None,
prefixes: HashMap::new(),
terms: HashMap::new(),
default_language: None,
}
}
pub fn parse(context: &Value) -> JsonLdResult<Self> {
let mut ctx = Self::default();
match context {
Value::Object(map) => {
ctx.apply_object(map)?;
}
Value::Array(arr) => {
for item in arr {
match item {
Value::Object(map) => ctx.apply_object(map)?,
Value::String(s) => {
ctx.base_iri = Some(s.clone());
}
Value::Null => {
ctx = Self::empty();
}
_ => {}
}
}
}
Value::String(s) => {
ctx.base_iri = Some(s.clone());
}
Value::Null => {
ctx = Self::empty();
}
_ => {
return Err(JsonLdError::InvalidContext(
"context must be an object, array, string, or null".into(),
))
}
}
Ok(ctx)
}
pub(crate) fn apply_object(&mut self, map: &Map<String, Value>) -> JsonLdResult<()> {
if let Some(base) = map.get("@base") {
match base {
Value::String(s) => self.base_iri = Some(s.clone()),
Value::Null => self.base_iri = None,
_ => return Err(JsonLdError::InvalidContext("@base must be a string".into())),
}
}
if let Some(vocab) = map.get("@vocab") {
match vocab {
Value::String(s) => self.vocab = Some(s.clone()),
Value::Null => self.vocab = None,
_ => {
return Err(JsonLdError::InvalidContext(
"@vocab must be a string".into(),
))
}
}
}
if let Some(lang) = map.get("@language") {
match lang {
Value::String(s) => self.default_language = Some(s.clone()),
Value::Null => self.default_language = None,
_ => {
return Err(JsonLdError::InvalidContext(
"@language must be a string".into(),
))
}
}
}
for (key, value) in map.iter() {
if key.starts_with('@') {
continue; }
match value {
Value::String(iri_or_prefix) => {
if iri_or_prefix.ends_with('/') || iri_or_prefix.ends_with('#') {
self.prefixes.insert(key.clone(), iri_or_prefix.clone());
} else {
let expanded = self.expand_term(iri_or_prefix);
self.terms.insert(
key.clone(),
TermDefinition {
iri: expanded,
container: None,
language: None,
type_coercion: None,
},
);
if iri_or_prefix.contains(':') && !iri_or_prefix.starts_with('@') {
self.prefixes.insert(key.clone(), iri_or_prefix.clone());
}
}
}
Value::Object(def_map) => {
let iri = if let Some(id_val) = def_map.get("@id") {
match id_val {
Value::String(s) => self.expand_term(s),
_ => {
return Err(JsonLdError::InvalidContext(format!(
"@id in term '{}' must be a string",
key
)))
}
}
} else {
if let Some(vocab) = &self.vocab.clone() {
format!("{}{}", vocab, key)
} else {
key.clone()
}
};
let container = def_map
.get("@container")
.and_then(|v| v.as_str())
.and_then(ContainerType::from_str);
let language = def_map
.get("@language")
.and_then(|v| v.as_str())
.map(String::from);
let type_coercion = def_map
.get("@type")
.and_then(|v| v.as_str())
.map(|t| self.expand_term(t));
self.terms.insert(
key.clone(),
TermDefinition {
iri,
container,
language,
type_coercion,
},
);
}
Value::Null => {
self.terms.remove(key);
self.prefixes.remove(key);
}
_ => {}
}
}
Ok(())
}
pub fn expand_term(&self, term: &str) -> String {
if term.starts_with('@') {
return term.to_string();
}
if let Some(def) = self.terms.get(term) {
return def.iri.clone();
}
if let Some(colon_pos) = term.find(':') {
let prefix = &term[..colon_pos];
let local = &term[colon_pos + 1..];
if !local.starts_with("//") {
if let Some(ns) = self.prefixes.get(prefix) {
return format!("{}{}", ns, local);
}
}
}
if is_absolute_iri(term) {
return term.to_string();
}
if let Some(vocab) = &self.vocab {
return format!("{}{}", vocab, term);
}
if let Some(base) = &self.base_iri {
return format!("{}{}", base, term);
}
term.to_string()
}
pub fn compact_iri(&self, iri: &str) -> String {
if iri.starts_with('@') {
return iri.to_string();
}
for (term, def) in &self.terms {
if def.iri == iri {
return term.clone();
}
}
if let Some(vocab) = &self.vocab {
if let Some(local) = iri.strip_prefix(vocab.as_str()) {
if !local.is_empty() && !local.contains('/') && !local.contains('#') {
return local.to_string();
}
}
}
let mut best: Option<(usize, String)> = None;
for (prefix, ns) in &self.prefixes {
if let Some(local) = iri.strip_prefix(ns.as_str()) {
if local.is_empty() {
continue;
}
let len = ns.len();
if best.as_ref().map_or(true, |(prev_len, _)| len > *prev_len) {
best = Some((len, format!("{}:{}", prefix, local)));
}
}
}
if let Some((_, compact)) = best {
return compact;
}
iri.to_string()
}
}
pub fn is_absolute_iri(s: &str) -> bool {
if let Some(pos) = s.find(':') {
let scheme = &s[..pos];
scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
&& !scheme.is_empty()
} else {
false
}
}
pub const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
pub const XSD_BOOLEAN: &str = "http://www.w3.org/2001/XMLSchema#boolean";
pub const XSD_INTEGER: &str = "http://www.w3.org/2001/XMLSchema#integer";
pub const XSD_DOUBLE: &str = "http://www.w3.org/2001/XMLSchema#double";
pub const RDF_LANG_STRING: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
static BLANK_NODE_COUNTER: AtomicU64 = AtomicU64::new(0);
pub(crate) fn next_blank_node() -> String {
format!("_:b{}", BLANK_NODE_COUNTER.fetch_add(1, Ordering::Relaxed))
}