json_ld_compaction/
node.rs

1use crate::{add_value, compact_iri, compact_property, Error, Options};
2use contextual::WithContext;
3use json_ld_context_processing::{Options as ProcessingOptions, Process, ProcessingMode};
4use json_ld_core::{Container, ContainerKind, Context, Id, Loader, Node, Term, Type};
5use json_ld_syntax::Keyword;
6use mown::Mown;
7use rdf_types::VocabularyMut;
8use std::hash::Hash;
9
10fn optional_string(s: Option<String>) -> json_syntax::Value {
11	s.map(Into::into)
12		.unwrap_or_else(|| json_syntax::Value::Null)
13}
14
15/// Compact the given indexed node.
16#[allow(clippy::too_many_arguments)]
17pub async fn compact_indexed_node_with<N, L>(
18	vocabulary: &mut N,
19	node: &Node<N::Iri, N::BlankId>,
20	index: Option<&str>,
21	mut active_context: &Context<N::Iri, N::BlankId>,
22	type_scoped_context: &Context<N::Iri, N::BlankId>,
23	active_property: Option<&str>,
24	loader: &L,
25	options: Options,
26) -> Result<json_syntax::Value, Error>
27where
28	N: VocabularyMut,
29	N::Iri: Clone + Hash + Eq,
30	N::BlankId: Clone + Hash + Eq,
31	L: Loader,
32{
33	// If active context has a previous context, the active context is not propagated.
34	// If element does not contain an @value entry, and element does not consist of
35	// a single @id entry, set active context to previous context from active context,
36	// as the scope of a term-scoped context does not apply when processing new node objects.
37	if !(node.is_empty() && node.id.is_some()) {
38		// does not consist of a single @id entry
39		if let Some(previous_context) = active_context.previous_context() {
40			active_context = previous_context
41		}
42	}
43
44	// If the term definition for active property in active context has a local context:
45	// FIXME https://github.com/w3c/json-ld-api/issues/502
46	//       Seems that the term definition should be looked up in `type_scoped_context`.
47	let mut active_context = Mown::Borrowed(active_context);
48	if let Some(active_property) = active_property {
49		if let Some(active_property_definition) = type_scoped_context.get(active_property) {
50			if let Some(local_context) = active_property_definition.context() {
51				active_context = Mown::Owned(
52					local_context
53						.process_with(
54							vocabulary,
55							active_context.as_ref(),
56							loader,
57							active_property_definition.base_url().cloned(),
58							ProcessingOptions::from(options).with_override(),
59						)
60						.await?
61						.into_processed(),
62				)
63			}
64		}
65	}
66
67	// let inside_reverse = active_property == Some("@reverse");
68	let mut result = json_syntax::Object::default();
69
70	if !node.types().is_empty() {
71		// If element has an @type entry, create a new array compacted types initialized by
72		// transforming each expanded type of that entry into its compacted form by IRI
73		// compacting expanded type. Then, for each term in compacted types ordered
74		// lexicographically:
75		let mut compacted_types = Vec::new();
76		for ty in node.types() {
77			let compacted_ty = compact_iri(
78				vocabulary,
79				type_scoped_context,
80				&ty.clone().into_term(),
81				true,
82				false,
83				options,
84			)?;
85			compacted_types.push(compacted_ty)
86		}
87
88		compacted_types.sort_by(|a, b| a.as_ref().unwrap().cmp(b.as_ref().unwrap()));
89
90		for term in &compacted_types {
91			if let Some(term_definition) = type_scoped_context.get(term.as_ref().unwrap().as_str())
92			{
93				if let Some(local_context) = term_definition.context() {
94					let processing_options = ProcessingOptions::from(options).without_propagation();
95					active_context = Mown::Owned(
96						local_context
97							.process_with(
98								vocabulary,
99								active_context.as_ref(),
100								loader,
101								term_definition.base_url().cloned(),
102								processing_options,
103							)
104							.await?
105							.into_processed(),
106					)
107				}
108			}
109		}
110	}
111
112	// For each key expanded property and value expanded value in element, ordered
113	// lexicographically by expanded property if ordered is true:
114	let mut expanded_entries: Vec<_> = node.properties().iter().collect();
115	if options.ordered {
116		let vocabulary: &N = vocabulary;
117		expanded_entries.sort_by(|(a, _), (b, _)| {
118			(**a)
119				.with(vocabulary)
120				.as_str()
121				.cmp((**b).with(vocabulary).as_str())
122		})
123	}
124
125	// If expanded property is @id:
126	if let Some(id_entry) = &node.id {
127		let id = id_entry.clone().into_term();
128
129		if node.is_empty() {
130			// This captures step 7:
131			// If element has an @value or @id entry and the result of using the
132			// Value Compaction algorithm, passing active context, active property,
133			// and element as value is a scalar, or the term definition for active property
134			// has a type mapping of @json, return that result.
135			//
136			// in the Value Compaction Algorithm, step 7:
137			// If value has an @id entry and has no other entries other than @index:
138			//
139			// If the type mapping of active property is set to @id,
140			// set result to the result of IRI compacting the value associated with the
141			// @id entry using false for vocab.
142			let type_mapping = match active_property {
143				Some(prop) => match active_context.get(prop) {
144					Some(def) => def.typ(),
145					None => None,
146				},
147				None => None,
148			};
149
150			if type_mapping == Some(&Type::Id) {
151				let compacted_value = compact_iri(
152					vocabulary,
153					active_context.as_ref(),
154					&id,
155					false,
156					false,
157					options,
158				)?;
159				return Ok(optional_string(compacted_value));
160			}
161
162			// Otherwise, if the type mapping of active property is set to @vocab,
163			// set result to the result of IRI compacting the value associated with the @id entry.
164			if type_mapping == Some(&Type::Vocab) {
165				let compacted_value = compact_iri(
166					vocabulary,
167					active_context.as_ref(),
168					&id,
169					true,
170					false,
171					options,
172				)?;
173				return Ok(optional_string(compacted_value));
174			}
175		}
176
177		// If expanded value is a string, then initialize compacted value by IRI
178		// compacting expanded value with vocab set to false.
179		let compacted_value = compact_iri(
180			vocabulary,
181			active_context.as_ref(),
182			&id,
183			false,
184			false,
185			options,
186		)?;
187
188		// Initialize alias by IRI compacting expanded property.
189		let alias = compact_iri(
190			vocabulary,
191			active_context.as_ref(),
192			&Term::Keyword(Keyword::Id),
193			true,
194			false,
195			options,
196		)?;
197
198		// Add an entry alias to result whose value is set to compacted value and continue
199		// to the next expanded property.
200		if let Some(key) = alias {
201			result.insert(key.into(), optional_string(compacted_value));
202		}
203	}
204
205	compact_types(
206		vocabulary,
207		&mut result,
208		node.types.as_deref(),
209		active_context.as_ref(),
210		type_scoped_context,
211		options,
212	)?;
213
214	// If expanded property is @reverse:
215	if let Some(reverse_properties) = node.reverse_properties_entry() {
216		if !reverse_properties.is_empty() {
217			// Initialize compacted value to the result of using this algorithm recursively,
218			// passing active context, @reverse for active property,
219			// expanded value for element, and the compactArrays and ordered flags.
220			let active_property = "@reverse";
221			if let Some(active_property_definition) = active_context.get(active_property) {
222				if let Some(local_context) = active_property_definition.context() {
223					active_context = Mown::Owned(
224						local_context
225							.process_with(
226								vocabulary,
227								active_context.as_ref(),
228								loader,
229								active_property_definition.base_url().cloned(),
230								ProcessingOptions::from(options).with_override(),
231							)
232							.await?
233							.into_processed(),
234					)
235				}
236			}
237
238			let mut reverse_result = json_syntax::Object::default();
239			for (expanded_property, expanded_value) in reverse_properties.iter() {
240				compact_property(
241					vocabulary,
242					&mut reverse_result,
243					expanded_property.clone().into(),
244					expanded_value.iter(),
245					active_context.as_ref(),
246					loader,
247					true,
248					options,
249				)
250				.await?;
251			}
252
253			// For each property and value in compacted value:
254			let mut reverse_map = json_syntax::Object::default();
255			for (property, mapped_value) in reverse_result.iter_mut() {
256				let mut value = json_syntax::Value::Null;
257				std::mem::swap(&mut value, &mut *mapped_value);
258
259				// If the term definition for property in the active context indicates that
260				// property is a reverse property
261				if let Some(term_definition) = active_context.get(property.as_str()) {
262					if term_definition.reverse_property() {
263						// Initialize as array to true if the container mapping for property in
264						// the active context includes @set, otherwise the negation of compactArrays.
265						let as_array = term_definition.container().contains(ContainerKind::Set)
266							|| !options.compact_arrays;
267
268						// Use add value to add value to the property entry in result using as array.
269						add_value(&mut result, property, value, as_array);
270						continue;
271					}
272				}
273
274				reverse_map.insert(property.clone(), value);
275			}
276
277			if !reverse_map.is_empty() {
278				// Initialize alias by IRI compacting @reverse.
279				let alias = compact_iri(
280					vocabulary,
281					active_context.as_ref(),
282					&Term::Keyword(Keyword::Reverse),
283					true,
284					false,
285					options,
286				)?;
287
288				// Set the value of the alias entry of result to compacted value.
289				result.insert(alias.unwrap().into(), reverse_map.into());
290			}
291		}
292	}
293
294	// If expanded property is @index and active property has a container mapping in
295	// active context that includes @index,
296	if let Some(index_entry) = index {
297		let mut index_container = false;
298		if let Some(active_property) = active_property {
299			if let Some(active_property_definition) = active_context.get(active_property) {
300				if active_property_definition
301					.container()
302					.contains(ContainerKind::Index)
303				{
304					// then the compacted result will be inside of an @index container,
305					// drop the @index entry by continuing to the next expanded property.
306					index_container = true;
307				}
308			}
309		}
310
311		if !index_container {
312			// Initialize alias by IRI compacting expanded property.
313			let alias = compact_iri(
314				vocabulary,
315				active_context.as_ref(),
316				&Term::Keyword(Keyword::Index),
317				true,
318				false,
319				options,
320			)?;
321
322			// Add an entry alias to result whose value is set to expanded value and continue with the next expanded property.
323			result.insert(alias.unwrap().into(), index_entry.into());
324		}
325	}
326
327	if let Some(graph_entry) = node.graph_entry() {
328		compact_property(
329			vocabulary,
330			&mut result,
331			Term::Keyword(Keyword::Graph),
332			graph_entry.iter(),
333			active_context.as_ref(),
334			loader,
335			false,
336			options,
337		)
338		.await?
339	}
340
341	for (expanded_property, expanded_value) in expanded_entries {
342		compact_property(
343			vocabulary,
344			&mut result,
345			expanded_property.clone().into(),
346			expanded_value.iter(),
347			active_context.as_ref(),
348			loader,
349			false,
350			options,
351		)
352		.await?
353	}
354
355	if let Some(included_entry) = node.included_entry() {
356		compact_property(
357			vocabulary,
358			&mut result,
359			Term::Keyword(Keyword::Included),
360			included_entry.iter(),
361			active_context.as_ref(),
362			loader,
363			false,
364			options,
365		)
366		.await?
367	}
368
369	Ok(result.into())
370}
371
372/// Compact the given list of types into the given `result` compacted object.
373fn compact_types<N>(
374	vocabulary: &mut N,
375	result: &mut json_syntax::Object,
376	types: Option<&[Id<N::Iri, N::BlankId>]>,
377	active_context: &Context<N::Iri, N::BlankId>,
378	type_scoped_context: &Context<N::Iri, N::BlankId>,
379	options: Options,
380) -> Result<(), Error>
381where
382	N: VocabularyMut,
383	N::Iri: Clone + Hash + Eq,
384	N::BlankId: Clone + Hash + Eq,
385{
386	// If expanded property is @type:
387	if let Some(types) = types {
388		if !types.is_empty() {
389			// If expanded value is a string,
390			// then initialize compacted value by IRI compacting expanded value using
391			// type-scoped context for active context.
392			let compacted_value = if types.len() == 1 {
393				optional_string(compact_iri(
394					vocabulary,
395					type_scoped_context,
396					&types[0].clone().into_term(),
397					true,
398					false,
399					options,
400				)?)
401			} else {
402				// Otherwise, expanded value must be a @type array:
403				// Initialize compacted value to an empty array.
404				let mut compacted_value = Vec::with_capacity(types.len());
405
406				// For each item expanded type in expanded value:
407				for ty in types.iter() {
408					let ty = ty.clone().into_term();
409
410					// Set term by IRI compacting expanded type using type-scoped context for active context.
411					let compacted_ty =
412						compact_iri(vocabulary, type_scoped_context, &ty, true, false, options)?;
413
414					// Append term, to compacted value.
415					compacted_value.push(optional_string(compacted_ty))
416				}
417
418				json_syntax::Value::Array(compacted_value.into_iter().collect())
419			};
420
421			// Initialize alias by IRI compacting expanded property.
422			let alias = compact_iri(
423				vocabulary,
424				active_context,
425				&Term::Keyword(Keyword::Type),
426				true,
427				false,
428				options,
429			)?
430			.unwrap();
431
432			// Initialize as array to true if processing mode is json-ld-1.1 and the
433			// container mapping for alias in the active context includes @set,
434			// otherwise to the negation of compactArrays.
435			let container_mapping = match active_context.get(alias.as_str()) {
436				Some(def) => def.container(),
437				None => Container::None,
438			};
439			let as_array = (options.processing_mode == ProcessingMode::JsonLd1_1
440				&& container_mapping.contains(ContainerKind::Set))
441				|| !options.compact_arrays;
442
443			// Use add value to add compacted value to the alias entry in result using as array.
444			add_value(result, &alias, compacted_value, as_array)
445		}
446	}
447
448	Ok(())
449}