json_ld_compaction_next/
lib.rs

1//! This library implements the [JSON-LD compaction algorithm](https://www.w3.org/TR/json-ld-api/#compaction-algorithms)
2//! for the [`json-ld` crate](https://crates.io/crates/json-ld).
3//!
4//! # Usage
5//!
6//! The compaction algorithm is provided by the [`Compact`] trait.
7use indexmap::IndexSet;
8use json_ld_context_processing_next::{Options as ProcessingOptions, Process};
9use json_ld_core_next::{
10	context::inverse::{LangSelection, TypeSelection},
11	object::Any,
12	Context, Indexed, Loader, ProcessingMode, Term, Value,
13};
14use json_ld_syntax_next::{ContainerKind, ErrorCode, Keyword};
15use json_syntax::object::Entry;
16use mown::Mown;
17use rdf_types::{vocabulary, VocabularyMut};
18use std::hash::Hash;
19
20mod document;
21mod iri;
22mod node;
23mod property;
24mod value;
25
26pub use document::*;
27pub(crate) use iri::*;
28use node::*;
29use property::*;
30use value::*;
31
32#[derive(Debug, thiserror::Error)]
33pub enum Error {
34	#[error("IRI confused with prefix")]
35	IriConfusedWithPrefix,
36
37	#[error("Invalid `@nest` value")]
38	InvalidNestValue,
39
40	#[error("Context processing failed: {0}")]
41	ContextProcessing(json_ld_context_processing_next::Error),
42}
43
44impl Error {
45	pub fn code(&self) -> ErrorCode {
46		match self {
47			Self::IriConfusedWithPrefix => ErrorCode::IriConfusedWithPrefix,
48			Self::InvalidNestValue => ErrorCode::InvalidNestValue,
49			Self::ContextProcessing(e) => e.code(),
50		}
51	}
52}
53
54impl From<json_ld_context_processing_next::Error> for Error {
55	fn from(e: json_ld_context_processing_next::Error) -> Self {
56		Self::ContextProcessing(e)
57	}
58}
59
60impl From<IriConfusedWithPrefix> for Error {
61	fn from(_: IriConfusedWithPrefix) -> Self {
62		Self::IriConfusedWithPrefix
63	}
64}
65
66pub type CompactFragmentResult = Result<json_syntax::Value, Error>;
67
68/// Compaction options.
69#[derive(Clone, Copy)]
70pub struct Options {
71	/// JSON-LD processing mode.
72	pub processing_mode: ProcessingMode,
73
74	/// Determines if IRIs are compacted relative to the provided base IRI or document location when compacting.
75	pub compact_to_relative: bool,
76
77	/// If set to `true`, arrays with just one element are replaced with that element during compaction.
78	/// If set to `false`, all arrays will remain arrays even if they have just one element.
79	pub compact_arrays: bool,
80
81	/// If set to `true`, properties are processed by lexical order.
82	/// If `false`, order is not considered in processing.
83	pub ordered: bool,
84}
85
86impl Options {
87	pub fn unordered(self) -> Self {
88		Self {
89			ordered: false,
90			..self
91		}
92	}
93}
94
95impl From<Options> for json_ld_context_processing_next::Options {
96	fn from(options: Options) -> json_ld_context_processing_next::Options {
97		json_ld_context_processing_next::Options {
98			processing_mode: options.processing_mode,
99			..Default::default()
100		}
101	}
102}
103
104impl From<json_ld_expansion_next::Options> for Options {
105	fn from(options: json_ld_expansion_next::Options) -> Options {
106		Options {
107			processing_mode: options.processing_mode,
108			ordered: options.ordered,
109			..Options::default()
110		}
111	}
112}
113
114impl Default for Options {
115	fn default() -> Options {
116		Options {
117			processing_mode: ProcessingMode::default(),
118			compact_to_relative: true,
119			compact_arrays: true,
120			ordered: false,
121		}
122	}
123}
124
125pub trait CompactFragment<I, B> {
126	#[allow(async_fn_in_trait)]
127	async fn compact_fragment_full<'a, N, L>(
128		&'a self,
129		vocabulary: &'a mut N,
130		active_context: &'a Context<I, B>,
131		type_scoped_context: &'a Context<I, B>,
132		active_property: Option<&'a str>,
133		loader: &'a L,
134		options: Options,
135	) -> CompactFragmentResult
136	where
137		N: VocabularyMut<Iri = I, BlankId = B>,
138		I: Clone + Hash + Eq,
139		B: Clone + Hash + Eq,
140		L: Loader;
141
142	#[allow(async_fn_in_trait)]
143	#[inline(always)]
144	async fn compact_fragment_with<'a, N, L>(
145		&'a self,
146		vocabulary: &'a mut N,
147		active_context: &'a Context<I, B>,
148		loader: &'a mut L,
149	) -> CompactFragmentResult
150	where
151		N: VocabularyMut<Iri = I, BlankId = B>,
152		I: Clone + Hash + Eq,
153		B: Clone + Hash + Eq,
154		L: Loader,
155	{
156		self.compact_fragment_full(
157			vocabulary,
158			active_context,
159			active_context,
160			None,
161			loader,
162			Options::default(),
163		)
164		.await
165	}
166
167	#[allow(async_fn_in_trait)]
168	#[inline(always)]
169	async fn compact_fragment<'a, L>(
170		&'a self,
171		active_context: &'a Context<I, B>,
172		loader: &'a mut L,
173	) -> CompactFragmentResult
174	where
175		(): VocabularyMut<Iri = I, BlankId = B>,
176		I: Clone + Hash + Eq,
177		B: Clone + Hash + Eq,
178		L: Loader,
179	{
180		self.compact_fragment_full(
181			vocabulary::no_vocabulary_mut(),
182			active_context,
183			active_context,
184			None,
185			loader,
186			Options::default(),
187		)
188		.await
189	}
190}
191
192enum TypeLangValue<'a, I> {
193	Type(TypeSelection<I>),
194	Lang(LangSelection<'a>),
195}
196
197/// Type that can be compacted with an index.
198pub trait CompactIndexedFragment<I, B> {
199	#[allow(async_fn_in_trait)]
200	#[allow(clippy::too_many_arguments)]
201	async fn compact_indexed_fragment<'a, N, L>(
202		&'a self,
203		vocabulary: &'a mut N,
204		index: Option<&'a str>,
205		active_context: &'a Context<I, B>,
206		type_scoped_context: &'a Context<I, B>,
207		active_property: Option<&'a str>,
208		loader: &'a L,
209		options: Options,
210	) -> CompactFragmentResult
211	where
212		N: VocabularyMut<Iri = I, BlankId = B>,
213		I: Clone + Hash + Eq,
214		B: Clone + Hash + Eq,
215		L: Loader;
216}
217
218impl<I, B, T: CompactIndexedFragment<I, B>> CompactFragment<I, B> for Indexed<T> {
219	async fn compact_fragment_full<'a, N, L>(
220		&'a self,
221		vocabulary: &'a mut N,
222		active_context: &'a Context<I, B>,
223		type_scoped_context: &'a Context<I, B>,
224		active_property: Option<&'a str>,
225		loader: &'a L,
226		options: Options,
227	) -> CompactFragmentResult
228	where
229		N: VocabularyMut<Iri = I, BlankId = B>,
230		I: Clone + Hash + Eq,
231		B: Clone + Hash + Eq,
232		L: Loader,
233	{
234		self.inner()
235			.compact_indexed_fragment(
236				vocabulary,
237				self.index(),
238				active_context,
239				type_scoped_context,
240				active_property,
241				loader,
242				options,
243			)
244			.await
245	}
246}
247
248impl<I, B, T: Any<I, B>> CompactIndexedFragment<I, B> for T {
249	async fn compact_indexed_fragment<'a, N, L>(
250		&'a self,
251		vocabulary: &'a mut N,
252		index: Option<&'a str>,
253		active_context: &'a Context<I, B>,
254		type_scoped_context: &'a Context<I, B>,
255		active_property: Option<&'a str>,
256		loader: &'a L,
257		options: Options,
258	) -> CompactFragmentResult
259	where
260		N: VocabularyMut<Iri = I, BlankId = B>,
261		I: Clone + Hash + Eq,
262		B: Clone + Hash + Eq,
263		L: Loader,
264	{
265		use json_ld_core_next::object::Ref;
266		match self.as_ref() {
267			Ref::Value(value) => {
268				compact_indexed_value_with(
269					vocabulary,
270					value,
271					index,
272					active_context,
273					active_property,
274					loader,
275					options,
276				)
277				.await
278			}
279			Ref::Node(node) => {
280				compact_indexed_node_with(
281					vocabulary,
282					node,
283					index,
284					active_context,
285					type_scoped_context,
286					active_property,
287					loader,
288					options,
289				)
290				.await
291			}
292			Ref::List(list) => {
293				let mut active_context = active_context;
294				// If active context has a previous context, the active context is not propagated.
295				// If element does not contain an @value entry, and element does not consist of
296				// a single @id entry, set active context to previous context from active context,
297				// as the scope of a term-scoped context does not apply when processing new node objects.
298				if let Some(previous_context) = active_context.previous_context() {
299					active_context = previous_context
300				}
301
302				// If the term definition for active property in active context has a local context:
303				// FIXME https://github.com/w3c/json-ld-api/issues/502
304				//       Seems that the term definition should be looked up in `type_scoped_context`.
305				let mut active_context = Mown::Borrowed(active_context);
306				let mut list_container = false;
307				if let Some(active_property) = active_property {
308					if let Some(active_property_definition) =
309						type_scoped_context.get(active_property)
310					{
311						if let Some(local_context) = active_property_definition.context() {
312							active_context = Mown::Owned(
313								local_context
314									.process_with(
315										vocabulary,
316										active_context.as_ref(),
317										loader,
318										active_property_definition.base_url().cloned(),
319										ProcessingOptions::from(options).with_override(),
320									)
321									.await?
322									.into_processed(),
323							)
324						}
325
326						list_container = active_property_definition
327							.container()
328							.contains(ContainerKind::List);
329					}
330				}
331
332				if list_container {
333					compact_collection_with(
334						vocabulary,
335						list.iter(),
336						active_context.as_ref(),
337						active_context.as_ref(),
338						active_property,
339						loader,
340						options,
341					)
342					.await
343				} else {
344					let mut result = json_syntax::Object::default();
345					compact_property(
346						vocabulary,
347						&mut result,
348						Term::Keyword(Keyword::List),
349						list,
350						active_context.as_ref(),
351						loader,
352						false,
353						options,
354					)
355					.await?;
356
357					// If expanded property is @index and active property has a container mapping in
358					// active context that includes @index,
359					if let Some(index) = index {
360						let mut index_container = false;
361						if let Some(active_property) = active_property {
362							if let Some(active_property_definition) =
363								active_context.get(active_property)
364							{
365								if active_property_definition
366									.container()
367									.contains(ContainerKind::Index)
368								{
369									// then the compacted result will be inside of an @index container,
370									// drop the @index entry by continuing to the next expanded property.
371									index_container = true;
372								}
373							}
374						}
375
376						if !index_container {
377							// Initialize alias by IRI compacting expanded property.
378							let alias = compact_key(
379								vocabulary,
380								active_context.as_ref(),
381								&Term::Keyword(Keyword::Index),
382								true,
383								false,
384								options,
385							)?;
386
387							// Add an entry alias to result whose value is set to expanded value and continue with the next expanded property.
388							result.insert(alias.unwrap(), json_syntax::Value::String(index.into()));
389						}
390					}
391
392					Ok(json_syntax::Value::Object(result))
393				}
394			}
395		}
396	}
397}
398
399/// Default value of `as_array` is false.
400fn add_value(map: &mut json_syntax::Object, key: &str, value: json_syntax::Value, as_array: bool) {
401	match map
402		.get_unique(key)
403		.ok()
404		.unwrap()
405		.map(|entry| entry.is_array())
406	{
407		Some(false) => {
408			let Entry { key, value } = map.remove_unique(key).ok().unwrap().unwrap();
409			map.insert(key, json_syntax::Value::Array(vec![value]));
410		}
411		None if as_array => {
412			map.insert(key.into(), json_syntax::Value::Array(Vec::new()));
413		}
414		_ => (),
415	}
416
417	match value {
418		json_syntax::Value::Array(values) => {
419			for value in values {
420				add_value(map, key, value, false)
421			}
422		}
423		value => {
424			if let Some(array) = map.get_unique_mut(key).ok().unwrap() {
425				array.as_array_mut().unwrap().push(value);
426				return;
427			}
428
429			map.insert(key.into(), value);
430		}
431	}
432}
433
434/// Get the `@value` field of a value object.
435fn value_value<I>(value: &Value<I>) -> json_syntax::Value {
436	use json_ld_core_next::object::Literal;
437	match value {
438		Value::Literal(lit, _ty) => match lit {
439			Literal::Null => json_syntax::Value::Null,
440			Literal::Boolean(b) => json_syntax::Value::Boolean(*b),
441			Literal::Number(n) => json_syntax::Value::Number(n.clone()),
442			Literal::String(s) => json_syntax::Value::String(s.as_str().into()),
443		},
444		Value::LangString(s) => json_syntax::Value::String(s.as_str().into()),
445		Value::Json(json) => json.clone(),
446	}
447}
448
449async fn compact_collection_with<'a, N, L, O, T>(
450	vocabulary: &'a mut N,
451	items: O,
452	active_context: &'a Context<N::Iri, N::BlankId>,
453	type_scoped_context: &'a Context<N::Iri, N::BlankId>,
454	active_property: Option<&'a str>,
455	loader: &'a L,
456	options: Options,
457) -> CompactFragmentResult
458where
459	N: VocabularyMut,
460	N::Iri: Clone + Hash + Eq,
461	N::BlankId: Clone + Hash + Eq,
462	T: 'a + CompactFragment<N::Iri, N::BlankId>,
463	O: 'a + Iterator<Item = &'a T>,
464	L: Loader,
465{
466	let mut result = Vec::new();
467
468	for item in items {
469		let compacted_item = Box::pin(item.compact_fragment_full(
470			vocabulary,
471			active_context,
472			type_scoped_context,
473			active_property,
474			loader,
475			options,
476		))
477		.await?;
478
479		if !compacted_item.is_null() {
480			result.push(compacted_item)
481		}
482	}
483
484	let mut list_or_set = false;
485	if let Some(active_property) = active_property {
486		if let Some(active_property_definition) = active_context.get(active_property) {
487			list_or_set = active_property_definition
488				.container()
489				.contains(ContainerKind::List)
490				|| active_property_definition
491					.container()
492					.contains(ContainerKind::Set);
493		}
494	}
495
496	if result.is_empty()
497		|| result.len() > 1
498		|| !options.compact_arrays
499		|| active_property == Some("@graph")
500		|| active_property == Some("@set")
501		|| list_or_set
502	{
503		return Ok(json_syntax::Value::Array(result.into_iter().collect()));
504	}
505
506	Ok(result.into_iter().next().unwrap())
507}
508
509impl<T: CompactFragment<I, B>, I, B> CompactFragment<I, B> for IndexSet<T> {
510	async fn compact_fragment_full<'a, N, L>(
511		&'a self,
512		vocabulary: &'a mut N,
513		active_context: &'a Context<I, B>,
514		type_scoped_context: &'a Context<I, B>,
515		active_property: Option<&'a str>,
516		loader: &'a L,
517		options: Options,
518	) -> CompactFragmentResult
519	where
520		N: VocabularyMut<Iri = I, BlankId = B>,
521		I: Clone + Hash + Eq,
522		B: Clone + Hash + Eq,
523		L: Loader,
524	{
525		compact_collection_with(
526			vocabulary,
527			self.iter(),
528			active_context,
529			type_scoped_context,
530			active_property,
531			loader,
532			options,
533		)
534		.await
535	}
536}
537
538impl<T: CompactFragment<I, B>, I, B> CompactFragment<I, B> for Vec<T> {
539	async fn compact_fragment_full<'a, N, L>(
540		&'a self,
541		vocabulary: &'a mut N,
542		active_context: &'a Context<I, B>,
543		type_scoped_context: &'a Context<I, B>,
544		active_property: Option<&'a str>,
545		loader: &'a L,
546		options: Options,
547	) -> CompactFragmentResult
548	where
549		N: VocabularyMut<Iri = I, BlankId = B>,
550		I: Clone + Hash + Eq,
551		B: Clone + Hash + Eq,
552		L: Loader,
553	{
554		compact_collection_with(
555			vocabulary,
556			self.iter(),
557			active_context,
558			type_scoped_context,
559			active_property,
560			loader,
561			options,
562		)
563		.await
564	}
565}
566
567impl<T: CompactFragment<I, B> + Send + Sync, I, B> CompactFragment<I, B> for [T] {
568	async fn compact_fragment_full<'a, N, L>(
569		&'a self,
570		vocabulary: &'a mut N,
571		active_context: &'a Context<I, B>,
572		type_scoped_context: &'a Context<I, B>,
573		active_property: Option<&'a str>,
574		loader: &'a L,
575		options: Options,
576	) -> CompactFragmentResult
577	where
578		N: VocabularyMut<Iri = I, BlankId = B>,
579		I: Clone + Hash + Eq,
580		B: Clone + Hash + Eq,
581		L: Loader,
582	{
583		compact_collection_with(
584			vocabulary,
585			self.iter(),
586			active_context,
587			type_scoped_context,
588			active_property,
589			loader,
590			options,
591		)
592		.await
593	}
594}