use crate::{Error, IriConfusedWithPrefix, Options, add_value, compact_iri, compact_property, iri::keyword_alias};
use contextual::WithContext;
use jsonld_context_processing::{Options as ProcessingOptions, Process, ProcessingMode};
use jsonld_core::{Container, ContainerKind, Context, ContextRef, Id, Loader, Node, Term, Type};
use jsonld_syntax::Keyword;
use rdfx::vocabulary::{Vocabulary, VocabularyMut};
use std::hash::Hash;
fn optional_string(s: Option<&str>) -> jstrict::Value {
s.map_or(jstrict::Value::Null, |s| jstrict::Value::String(s.into()))
}
pub async fn compact_indexed_node_with<N, L>(
vocabulary: &mut N,
node: &Node<N::Iri, N::BlankId>,
index: Option<&str>,
mut active_context: &Context<N::Iri, N::BlankId>,
type_scoped_context: &Context<N::Iri, N::BlankId>,
active_property: Option<&str>,
loader: &L,
options: Options,
) -> Result<jstrict::Value, Error<L::Error>>
where
N: VocabularyMut,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
L: Loader,
{
if !(node.is_empty() && node.id.is_some()) {
if let Some(previous_context) = active_context.previous_context() {
active_context = previous_context;
}
}
let mut active_context = ContextRef::Borrowed(active_context);
if let Some(active_property) = active_property
&& let Some(active_property_definition) = type_scoped_context.get(active_property)
&& let Some(local_context) = active_property_definition.context()
{
active_context = ContextRef::owned(
local_context
.process_with(
vocabulary,
active_context.as_ref(),
loader,
active_property_definition.base_url().cloned(),
ProcessingOptions::from(options).with_override(),
)
.await?
.into_processed(),
);
}
if node.is_empty()
&& let Some(id) = &node.id
{
return Ok(compact_node_reference(
vocabulary,
id,
index,
active_context.as_ref(),
active_property,
options,
)?);
}
let mut result = jstrict::Object::default();
if !node.types().is_empty() {
let mut compacted_types = Vec::new();
for ty in node.types() {
let compacted_ty = compact_iri(vocabulary, type_scoped_context, &ty.clone().into_term(), true, false, options)?;
compacted_types.push(compacted_ty);
}
compacted_types.sort_by(|a, b| a.as_deref().cmp(&b.as_deref()));
for term in compacted_types.iter().flatten() {
if let Some(term_definition) = type_scoped_context.get(&**term)
&& let Some(local_context) = term_definition.context()
{
let processing_options = ProcessingOptions::from(options).without_propagation();
active_context = ContextRef::owned(
local_context
.process_with(
vocabulary,
active_context.as_ref(),
loader,
term_definition.base_url().cloned(),
processing_options,
)
.await?
.into_processed(),
);
}
}
}
let expanded_entries: Vec<_> = if options.ordered {
let vocabulary: &N = vocabulary;
let mut decorated: Vec<(&str, _)> = node.properties().iter().map(|entry| (entry.0.with(vocabulary).as_str(), entry)).collect();
decorated.sort_by(|a, b| a.0.cmp(b.0));
decorated.into_iter().map(|(_, entry)| entry).collect()
} else {
node.properties().iter().collect()
};
if let Some(id_entry) = &node.id {
insert_id(vocabulary, &mut result, id_entry, active_context.as_ref(), options)?;
}
compact_types(
vocabulary,
&mut result,
node.types.as_deref(),
active_context.as_ref(),
type_scoped_context,
options,
)?;
if let Some(reverse_properties) = node.reverse_properties_entry()
&& !reverse_properties.is_empty()
{
let active_property = "@reverse";
if let Some(active_property_definition) = active_context.get(active_property)
&& let Some(local_context) = active_property_definition.context()
{
active_context = ContextRef::owned(
local_context
.process_with(
vocabulary,
active_context.as_ref(),
loader,
active_property_definition.base_url().cloned(),
ProcessingOptions::from(options).with_override(),
)
.await?
.into_processed(),
);
}
let mut reverse_result = jstrict::Object::default();
for (expanded_property, expanded_value) in reverse_properties {
compact_property(
vocabulary,
&mut reverse_result,
expanded_property.clone().into(),
expanded_value.iter(),
active_context.as_ref(),
loader,
true,
options,
)
.await?;
}
let mut reverse_map = jstrict::Object::default();
for (property, mapped_value) in &mut reverse_result {
let mut value = jstrict::Value::Null;
std::mem::swap(&mut value, &mut *mapped_value);
if let Some(term_definition) = active_context.get(property.as_str())
&& term_definition.reverse_property()
{
let as_array = term_definition.container().contains(ContainerKind::Set) || !options.compact_arrays;
add_value(&mut result, property, value, as_array);
continue;
}
reverse_map.insert(property.clone(), value);
}
if !reverse_map.is_empty() {
let alias = keyword_alias(vocabulary, active_context.as_ref(), options, Keyword::Reverse);
result.insert(alias.into(), reverse_map.into());
}
}
insert_index(vocabulary, &mut result, index, active_context.as_ref(), active_property, options);
if let Some(graph_entry) = node.graph_entry() {
compact_property(
vocabulary,
&mut result,
Term::Keyword(Keyword::Graph),
graph_entry.iter(),
active_context.as_ref(),
loader,
false,
options,
)
.await?;
}
for (expanded_property, expanded_value) in expanded_entries {
compact_property(
vocabulary,
&mut result,
expanded_property.clone().into(),
expanded_value.iter(),
active_context.as_ref(),
loader,
false,
options,
)
.await?;
}
if let Some(included_entry) = node.included_entry() {
compact_property(
vocabulary,
&mut result,
Term::Keyword(Keyword::Included),
included_entry.iter(),
active_context.as_ref(),
loader,
false,
options,
)
.await?;
}
Ok(result.into())
}
pub fn compact_node_reference<N>(
vocabulary: &N,
id: &Id<N::Iri, N::BlankId>,
index: Option<&str>,
active_context: &Context<N::Iri, N::BlankId>,
active_property: Option<&str>,
options: Options,
) -> Result<jstrict::Value, IriConfusedWithPrefix>
where
N: Vocabulary,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
{
let type_mapping = match active_property {
Some(prop) => match active_context.get(prop) {
Some(def) => def.typ(),
None => None,
},
None => None,
};
if type_mapping == Some(&Type::Id) {
let compacted_value = compact_iri(vocabulary, active_context, &id.clone().into_term(), false, false, options)?;
return Ok(optional_string(compacted_value.as_deref()));
}
if type_mapping == Some(&Type::Vocab) {
let compacted_value = compact_iri(vocabulary, active_context, &id.clone().into_term(), true, false, options)?;
return Ok(optional_string(compacted_value.as_deref()));
}
let mut result = jstrict::Object::default();
insert_id(vocabulary, &mut result, id, active_context, options)?;
insert_index(vocabulary, &mut result, index, active_context, active_property, options);
Ok(result.into())
}
fn insert_id<N>(
vocabulary: &N,
result: &mut jstrict::Object,
id: &Id<N::Iri, N::BlankId>,
active_context: &Context<N::Iri, N::BlankId>,
options: Options,
) -> Result<(), IriConfusedWithPrefix>
where
N: Vocabulary,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
{
let compacted_value = compact_iri(vocabulary, active_context, &id.clone().into_term(), false, false, options)?;
let alias = keyword_alias(vocabulary, active_context, options, Keyword::Id);
result.insert(alias.into(), optional_string(compacted_value.as_deref()));
Ok(())
}
fn insert_index<N>(
vocabulary: &N,
result: &mut jstrict::Object,
index: Option<&str>,
active_context: &Context<N::Iri, N::BlankId>,
active_property: Option<&str>,
options: Options,
) where
N: Vocabulary,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
{
if let Some(index_entry) = index {
let mut index_container = false;
if let Some(active_property) = active_property
&& let Some(active_property_definition) = active_context.get(active_property)
&& active_property_definition.container().contains(ContainerKind::Index)
{
index_container = true;
}
if !index_container {
let alias = keyword_alias(vocabulary, active_context, options, Keyword::Index);
result.insert(alias.into(), index_entry.into());
}
}
}
#[must_use]
pub fn types_as_array<I, B>(active_context: &Context<I, B>, type_alias: &str, options: Options) -> bool {
let container_mapping = match active_context.get(type_alias) {
Some(def) => def.container(),
None => Container::None,
};
(options.processing_mode == ProcessingMode::JsonLd1_1 && container_mapping.contains(ContainerKind::Set)) || !options.compact_arrays
}
fn compact_types<N, E>(
vocabulary: &mut N,
result: &mut jstrict::Object,
types: Option<&[Id<N::Iri, N::BlankId>]>,
active_context: &Context<N::Iri, N::BlankId>,
type_scoped_context: &Context<N::Iri, N::BlankId>,
options: Options,
) -> Result<(), Error<E>>
where
N: VocabularyMut,
N::Iri: Clone + Hash + Eq,
N::BlankId: Clone + Hash + Eq,
{
if let Some(types) = types
&& !types.is_empty()
{
let compacted_value = if types.len() == 1 {
let arc = compact_iri(vocabulary, type_scoped_context, &types[0].clone().into_term(), true, false, options)?;
optional_string(arc.as_deref())
} else {
let mut compacted_value = Vec::with_capacity(types.len());
for ty in types {
let ty = ty.clone().into_term();
let compacted_ty = compact_iri(vocabulary, type_scoped_context, &ty, true, false, options)?;
compacted_value.push(optional_string(compacted_ty.as_deref()));
}
jstrict::Value::Array(compacted_value.into_iter().collect())
};
let alias = keyword_alias(vocabulary, active_context, options, Keyword::Type);
let as_array = types_as_array(active_context, alias, options);
add_value(result, alias, compacted_value, as_array);
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::{
CompactFragment,
test_fixtures::{VALUE_CONTEXT, compact_one_value_document, expanded_object, json, processed_context},
};
use jsonld_core::NoLoader;
use rdfx::vocabulary::{no_vocabulary, no_vocabulary_mut};
#[tokio::test]
async fn compact_node_reference_matches_the_compaction_of_a_one_value_document() {
let context = processed_context(VALUE_CONTEXT).await;
let cases = [
(
"https://example.com/object",
r#"{ "@id": "https://example.com/Building" }"#,
"object",
r#""ex:Building""#,
),
(
"https://example.com/objectVocab",
r#"{ "@id": "https://example.com/Building" }"#,
"objectVocab",
r#""Building""#,
),
(
"https://example.com/objectVocab",
r#"{ "@id": "https://vocab.example/Thing" }"#,
"objectVocab",
r#""Thing""#,
),
(
"https://example.com/text",
r#"{ "@id": "urn:ngsi-ld:Thing:1" }"#,
"text",
r#"{ "id": "urn:ngsi-ld:Thing:1" }"#,
),
("https://example.com/text", r#"{ "@id": "_:b0" }"#, "text", r#"{ "id": "_:b0" }"#),
("https://example.com/object", r#"{ "@id": "urn:x", "@index": "i" }"#, "object", r#""urn:x""#),
(
"https://example.com/text",
r#"{ "@id": "urn:x", "@index": "i" }"#,
"text",
r#"{ "id": "urn:x", "@index": "i" }"#,
),
];
for (property, reference, expected_key, expected) in cases {
let (key, written) = compact_one_value_document(&context, property, reference).await;
assert_eq!(key, expected_key, "key of {reference}");
let object = expanded_object(reference);
let compacted = compact_node_reference(no_vocabulary(), object.id().unwrap(), object.index(), &context, Some(&key), Options::default()).unwrap();
assert_eq!(compacted, json(expected), "compaction of {reference} under {key}");
assert_eq!(compacted, written, "{reference} compacted alone and in a document");
}
}
#[tokio::test]
async fn types_as_array_follows_the_container_of_the_type_alias() {
let node = expanded_object(r#"{ "@id": "urn:x", "@type": ["https://example.com/Building"] }"#);
let cases = [
(
r#"{ "type": "@type", "ex": "https://example.com/" }"#,
false,
r#"{ "@id": "urn:x", "type": "ex:Building" }"#,
),
(
r#"{ "type": { "@id": "@type", "@container": "@set" }, "ex": "https://example.com/" }"#,
true,
r#"{ "@id": "urn:x", "type": ["ex:Building"] }"#,
),
];
for (source, as_array, expected) in cases {
let context = processed_context(source).await;
let alias = keyword_alias(no_vocabulary(), &context, Options::default(), Keyword::Type);
assert_eq!(types_as_array(&context, alias, Options::default()), as_array, "{source}");
let compacted = node
.compact_fragment_full(no_vocabulary_mut(), &context, &context, None, &NoLoader, Options::default())
.await
.unwrap();
assert_eq!(compacted, json(expected), "{source}");
}
}
}