pub mod algorithm;
pub mod context;
#[cfg(test)]
mod tests;
pub use algorithm::{compact_array, compact_node, compact_value};
pub use context::{compact_iri, create_compact_context, find_term};
use indexmap::IndexMap;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq)]
pub enum JsonLdValue {
Null,
Bool(bool),
Number(f64),
Str(String),
Array(Vec<JsonLdValue>),
Object(IndexMap<String, JsonLdValue>),
}
impl JsonLdValue {
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Str(s) => Some(s),
_ => None,
}
}
pub fn as_object(&self) -> Option<&IndexMap<String, JsonLdValue>> {
match self {
Self::Object(m) => Some(m),
_ => None,
}
}
pub fn as_array(&self) -> Option<&[JsonLdValue]> {
match self {
Self::Array(a) => Some(a),
_ => None,
}
}
pub fn into_array_if_not(self) -> Vec<JsonLdValue> {
match self {
Self::Array(a) => a,
other => vec![other],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContainerType {
List,
Set,
Language,
Index,
Id,
Type,
Graph,
}
#[derive(Debug, Clone)]
pub struct TermDefinition {
pub iri_mapping: Option<String>,
pub prefix_flag: bool,
pub protected: bool,
pub reverse_property: bool,
pub container: Vec<ContainerType>,
pub language: Option<String>,
pub direction: Option<String>,
pub nest: Option<String>,
pub type_mapping: Option<String>,
}
impl TermDefinition {
pub fn simple(iri: impl Into<String>) -> Self {
Self {
iri_mapping: Some(iri.into()),
prefix_flag: false,
protected: false,
reverse_property: false,
container: Vec::new(),
language: None,
direction: None,
nest: None,
type_mapping: None,
}
}
pub fn prefix(iri: impl Into<String>) -> Self {
Self {
iri_mapping: Some(iri.into()),
prefix_flag: true,
protected: false,
reverse_property: false,
container: Vec::new(),
language: None,
direction: None,
nest: None,
type_mapping: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct JsonLdContext {
pub terms: HashMap<String, TermDefinition>,
pub vocab: Option<String>,
pub base: Option<String>,
pub language: Option<String>,
pub direction: Option<String>,
}
impl JsonLdContext {
pub fn new() -> Self {
Self::default()
}
pub fn add_prefix(&mut self, prefix: impl Into<String>, iri: impl Into<String>) {
self.terms
.insert(prefix.into(), TermDefinition::prefix(iri.into()));
}
pub fn add_term(&mut self, term: impl Into<String>, def: TermDefinition) {
self.terms.insert(term.into(), def);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProcessingMode {
JsonLd10,
#[default]
JsonLd11,
}
#[derive(Debug, Clone)]
pub struct CompactionOptions {
pub compact_arrays: bool,
pub processing_mode: ProcessingMode,
pub ordered: bool,
pub base: Option<String>,
}
impl Default for CompactionOptions {
fn default() -> Self {
Self {
compact_arrays: true,
processing_mode: ProcessingMode::JsonLd11,
ordered: false,
base: None,
}
}
}
#[derive(Debug, Error)]
pub enum CompactionError {
#[error("Invalid context: {0}")]
InvalidContext(String),
#[error("Invalid IRI: {0}")]
InvalidIri(String),
#[error("Processing error: {0}")]
ProcessingError(String),
#[error("Term collision: term '{term}' is already defined with a different mapping")]
CollisionError {
term: String,
},
#[error("Protected term redefinition: cannot redefine protected term '{0}'")]
ProtectedTermRedefinition(String),
}
pub fn compact(
input: &JsonLdValue,
context: &JsonLdContext,
options: &CompactionOptions,
) -> Result<JsonLdValue, CompactionError> {
let compacted_input = match input {
JsonLdValue::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
let c = compact_element(item, context, None, options)?;
if !c.is_null() {
out.push(c);
}
}
if out.len() == 1 && options.compact_arrays {
out.into_iter().next().unwrap_or(JsonLdValue::Null)
} else {
JsonLdValue::Array(out)
}
}
other => compact_element(other, context, None, options)?,
};
let ctx_value = create_compact_context(context);
let mut result: IndexMap<String, JsonLdValue> = IndexMap::new();
if !ctx_value.is_null() {
result.insert("@context".to_string(), ctx_value);
}
match compacted_input {
JsonLdValue::Object(map) => {
for (k, v) in map {
result.insert(k, v);
}
}
JsonLdValue::Null => {}
other => {
result.insert("@graph".to_string(), other);
}
}
Ok(JsonLdValue::Object(result))
}
fn compact_element(
value: &JsonLdValue,
ctx: &JsonLdContext,
active_property: Option<&str>,
options: &CompactionOptions,
) -> Result<JsonLdValue, CompactionError> {
match value {
JsonLdValue::Object(map) => {
if map.contains_key("@value") {
compact_value(ctx, active_property, value)
} else if map.contains_key("@list") {
let list_items = match map.get("@list") {
Some(JsonLdValue::Array(a)) => a.as_slice(),
_ => &[],
};
compact_array(ctx, active_property.unwrap_or("@list"), list_items, options)
} else {
compact_node(ctx, ctx, active_property, map, options)
}
}
JsonLdValue::Array(items) => {
compact_array(ctx, active_property.unwrap_or("@graph"), items, options)
}
other => Ok(other.clone()),
}
}