json_ld_context_processing/algorithm/
iri.rs

1use std::hash::Hash;
2
3use super::{DefinedTerms, Environment, Merged};
4use crate::{Error, Options, ProcessingStack, Warning, WarningHandler};
5use contextual::WithContext;
6use iref::{Iri, IriRef};
7use json_ld_core::{warning, Context, Id, Loader, Term};
8use json_ld_syntax::{self as syntax, context::definition::Key, ExpandableRef, Nullable};
9use rdf_types::{
10	vocabulary::{BlankIdVocabulary, IriVocabulary},
11	BlankId, Vocabulary, VocabularyMut,
12};
13use syntax::{context::definition::KeyOrKeywordRef, is_keyword_like, CompactIri};
14
15pub struct MalformedIri(pub String);
16
17impl From<MalformedIri> for Warning {
18	fn from(MalformedIri(s): MalformedIri) -> Self {
19		Self::MalformedIri(s)
20	}
21}
22
23/// Result of the [`expand_iri_with`] function.
24pub type ExpandIriResult<T, B> = Result<Option<Term<T, B>>, Error>;
25
26/// Default values for `document_relative` and `vocab` should be `false` and `true`.
27#[allow(clippy::too_many_arguments)]
28pub async fn expand_iri_with<'a, N, L, W>(
29	mut env: Environment<'a, N, L, W>,
30	active_context: &'a mut Context<N::Iri, N::BlankId>,
31	value: Nullable<ExpandableRef<'a>>,
32	document_relative: bool,
33	vocab: Option<Action>,
34	local_context: &'a Merged<'a>,
35	defined: &'a mut DefinedTerms,
36	remote_contexts: ProcessingStack<N::Iri>,
37	options: Options,
38) -> ExpandIriResult<N::Iri, N::BlankId>
39where
40	N: VocabularyMut,
41	N::Iri: Clone + Eq + Hash,
42	N::BlankId: Clone + PartialEq,
43	L: Loader,
44	W: WarningHandler<N>,
45{
46	match value {
47		Nullable::Null => Ok(Some(Term::Null)),
48		Nullable::Some(ExpandableRef::Keyword(k)) => Ok(Some(Term::Keyword(k))),
49		Nullable::Some(ExpandableRef::String(value)) => {
50			if is_keyword_like(value) {
51				return Ok(Some(Term::Null));
52			}
53
54			// If `local_context` is not null, it contains an entry with a key that equals value, and the
55			// value of the entry for value in defined is not true, invoke the Create Term Definition
56			// algorithm, passing active context, local context, value as term, and defined. This will
57			// ensure that a term definition is created for value in active context during Context
58			// Processing.
59			Box::pin(super::define(
60				Environment {
61					vocabulary: env.vocabulary,
62					loader: env.loader,
63					warnings: env.warnings,
64				},
65				active_context,
66				local_context,
67				value.into(),
68				defined,
69				remote_contexts.clone(),
70				None,
71				false,
72				options.with_no_override(),
73			))
74			.await?;
75
76			if let Some(term_definition) = active_context.get(value) {
77				// If active context has a term definition for value, and the associated IRI mapping
78				// is a keyword, return that keyword.
79				if let Some(value) = term_definition.value() {
80					if value.is_keyword() {
81						return Ok(Some(value.clone()));
82					}
83				}
84
85				// If vocab is true and the active context has a term definition for value, return the
86				// associated IRI mapping.
87				if vocab.is_some() {
88					return match term_definition.value() {
89						Some(value) => Ok(Some(value.clone())),
90						None => Ok(Some(Term::Null)),
91					};
92				}
93			}
94
95			if value.find(':').map(|i| i > 0).unwrap_or(false) {
96				if let Ok(blank_id) = BlankId::new(value) {
97					return Ok(Some(Term::Id(Id::blank(
98						env.vocabulary.insert_blank_id(blank_id),
99					))));
100				}
101
102				if value == "_:" {
103					return Ok(Some(Term::Id(Id::Invalid("_:".to_string()))));
104				}
105
106				if let Ok(compact_iri) = CompactIri::new(value) {
107					// If local context is not null, it contains a `prefix` entry, and the value of the
108					// prefix entry in defined is not true, invoke the Create Term Definition
109					// algorithm, passing active context, local context, prefix as term, and defined.
110					// This will ensure that a term definition is created for prefix in active context
111					// during Context Processing.
112					Box::pin(super::define(
113						Environment {
114							vocabulary: env.vocabulary,
115							loader: env.loader,
116							warnings: env.warnings,
117						},
118						active_context,
119						local_context,
120						KeyOrKeywordRef::Key(compact_iri.prefix().into()),
121						defined,
122						remote_contexts,
123						None,
124						false,
125						options.with_no_override(),
126					))
127					.await?;
128
129					// If active context contains a term definition for prefix having a non-null IRI
130					// mapping and the prefix flag of the term definition is true, return the result
131					// of concatenating the IRI mapping associated with prefix and suffix.
132					let prefix_key = Key::from(compact_iri.prefix().to_string());
133					if let Some(term_definition) = active_context.get_normal(&prefix_key) {
134						if term_definition.prefix {
135							if let Some(mapping) = &term_definition.value {
136								let mut result =
137									mapping.with(&*env.vocabulary).as_str().to_string();
138								result.push_str(compact_iri.suffix());
139
140								return Ok(Some(Term::Id(Id::from_string_in(
141									env.vocabulary,
142									result,
143								))));
144							}
145						}
146					}
147				}
148
149				if let Ok(iri) = Iri::new(value) {
150					return Ok(Some(Term::Id(Id::iri(env.vocabulary.insert(iri)))));
151				}
152			}
153
154			// If vocab is true, and active context has a vocabulary mapping, return the result of
155			// concatenating the vocabulary mapping with value.
156			if let Some(action) = vocab {
157				match active_context.vocabulary() {
158					Some(Term::Id(mapping)) => {
159						return match action {
160							Action::Keep => {
161								let mut result =
162									mapping.with(&*env.vocabulary).as_str().to_string();
163								result.push_str(value);
164
165								Ok(Some(Term::Id(Id::from_string_in(env.vocabulary, result))))
166							}
167							Action::Drop => Ok(None),
168							Action::Reject => Err(Error::ForbiddenVocab),
169						}
170					}
171					Some(_) => return Ok(Some(invalid_iri(&mut env, value.to_string()))),
172					None => (),
173				}
174			}
175
176			// Otherwise, if document relative is true set value to the result of resolving value
177			// against the base IRI from active context. Only the basic algorithm in section 5.2 of
178			// [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization
179			// are performed. Characters additionally allowed in IRI references are treated in the
180			// same way that unreserved characters are treated in URI references, per section 6.5 of
181			// [RFC3987].
182			if document_relative {
183				if let Ok(iri_ref) = IriRef::new(value) {
184					if let Some(iri) =
185						super::resolve_iri(env.vocabulary, iri_ref, active_context.base_iri())
186					{
187						return Ok(Some(Term::from(iri)));
188					}
189				}
190			}
191
192			// Return value as is.
193			Ok(Some(invalid_iri(&mut env, value.to_string())))
194		}
195	}
196}
197
198fn invalid_iri<N, L, W: json_ld_core::warning::Handler<N, Warning>>(
199	env: &mut Environment<N, L, W>,
200	value: String,
201) -> Term<N::Iri, N::BlankId>
202where
203	N: Vocabulary,
204{
205	env.warnings
206		.handle(env.vocabulary, MalformedIri(value.clone()).into());
207	Term::Id(Id::Invalid(value))
208}
209
210#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
211pub enum Action {
212	#[default]
213	Keep,
214	Drop,
215	Reject,
216}
217
218impl Action {
219	pub fn is_reject(&self) -> bool {
220		matches!(self, Self::Reject)
221	}
222}
223
224#[derive(Debug)]
225pub struct RejectVocab;
226
227pub type IriExpansionResult<N> =
228	Result<Option<Term<<N as IriVocabulary>::Iri, <N as BlankIdVocabulary>::BlankId>>, RejectVocab>;
229
230/// Default values for `document_relative` and `vocab` should be `false` and `true`.
231pub fn expand_iri_simple<W, N, L, H>(
232	env: &mut Environment<N, L, H>,
233	active_context: &Context<N::Iri, N::BlankId>,
234	value: Nullable<ExpandableRef>,
235	document_relative: bool,
236	vocab: Option<Action>,
237) -> IriExpansionResult<N>
238where
239	N: VocabularyMut,
240	N::Iri: Clone,
241	N::BlankId: Clone,
242	W: From<MalformedIri>,
243	H: warning::Handler<N, W>,
244{
245	match value {
246		Nullable::Null => Ok(Some(Term::Null)),
247		Nullable::Some(ExpandableRef::Keyword(k)) => Ok(Some(Term::Keyword(k))),
248		Nullable::Some(ExpandableRef::String(value)) => {
249			if is_keyword_like(value) {
250				return Ok(Some(Term::Null));
251			}
252
253			if let Some(term_definition) = active_context.get(value) {
254				// If active context has a term definition for value, and the associated IRI mapping
255				// is a keyword, return that keyword.
256				if let Some(value) = term_definition.value() {
257					if value.is_keyword() {
258						return Ok(Some(value.clone()));
259					}
260				}
261
262				// If vocab is true and the active context has a term definition for value, return the
263				// associated IRI mapping.
264				if vocab.is_some() {
265					return match term_definition.value() {
266						Some(value) => Ok(Some(value.clone())),
267						None => Ok(Some(Term::Null)),
268					};
269				}
270			}
271
272			if value.find(':').map(|i| i > 0).unwrap_or(false) {
273				if let Ok(blank_id) = BlankId::new(value) {
274					return Ok(Some(Term::Id(Id::blank(
275						env.vocabulary.insert_blank_id(blank_id),
276					))));
277				}
278
279				if value == "_:" {
280					return Ok(Some(Term::Id(Id::Invalid("_:".to_string()))));
281				}
282
283				if let Ok(compact_iri) = CompactIri::new(value) {
284					// If active context contains a term definition for prefix having a non-null IRI
285					// mapping and the prefix flag of the term definition is true, return the result
286					// of concatenating the IRI mapping associated with prefix and suffix.
287					let prefix_key = Key::from(compact_iri.prefix().to_string());
288					if let Some(term_definition) = active_context.get_normal(&prefix_key) {
289						if term_definition.prefix {
290							if let Some(mapping) = &term_definition.value {
291								let mut result =
292									mapping.with(&*env.vocabulary).as_str().to_string();
293								result.push_str(compact_iri.suffix());
294
295								return Ok(Some(Term::Id(Id::from_string_in(
296									env.vocabulary,
297									result,
298								))));
299							}
300						}
301					}
302				}
303
304				if let Ok(iri) = Iri::new(value) {
305					return Ok(Some(Term::Id(Id::iri(env.vocabulary.insert(iri)))));
306				}
307			}
308
309			// If vocab is true, and active context has a vocabulary mapping, return the result of
310			// concatenating the vocabulary mapping with value.
311			if let Some(action) = vocab {
312				match active_context.vocabulary() {
313					Some(Term::Id(mapping)) => {
314						return match action {
315							Action::Keep => {
316								let mut result =
317									mapping.with(&*env.vocabulary).as_str().to_string();
318								result.push_str(value);
319
320								Ok(Some(Term::Id(Id::from_string_in(env.vocabulary, result))))
321							}
322							Action::Drop => Ok(None),
323							Action::Reject => Err(RejectVocab),
324						}
325					}
326					Some(_) => return Ok(Some(invalid_iri_simple(env, value.to_string()))),
327					None => (),
328				}
329			}
330
331			// Otherwise, if document relative is true set value to the result of resolving value
332			// against the base IRI from active context. Only the basic algorithm in section 5.2 of
333			// [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization
334			// are performed. Characters additionally allowed in IRI references are treated in the
335			// same way that unreserved characters are treated in URI references, per section 6.5 of
336			// [RFC3987].
337			if document_relative {
338				if let Ok(iri_ref) = IriRef::new(value) {
339					if let Some(iri) =
340						super::resolve_iri(env.vocabulary, iri_ref, active_context.base_iri())
341					{
342						return Ok(Some(Term::from(iri)));
343					}
344				}
345			}
346
347			// Return value as is.
348			Ok(Some(invalid_iri_simple(env, value.to_string())))
349		}
350	}
351}
352
353fn invalid_iri_simple<W, N, L, H>(
354	env: &mut Environment<N, L, H>,
355	value: String,
356) -> Term<N::Iri, N::BlankId>
357where
358	N: Vocabulary,
359	W: From<MalformedIri>,
360	H: warning::Handler<N, W>,
361{
362	env.warnings
363		.handle(env.vocabulary, MalformedIri(value.clone()).into());
364	Term::Id(Id::Invalid(value))
365}