1use crate::{Options, TypeLangValue};
2use contextual::WithContext;
3use jsonld_core::{
4 Container,
5 Context,
6 Indexed,
7 Nullable,
8 Object,
9 ProcessingMode,
10 Term,
11 Type,
12 Value,
13 context::{
14 CACHED_KEYWORDS,
15 CompactIriKeyRef,
16 KeywordAliases,
17 inverse::{LangSelection, Selection, TypeSelection},
18 },
19 object,
20};
21use jsonld_syntax::{Keyword, is_keyword, is_keyword_like};
22use rdfx::vocabulary::Vocabulary;
23use smallvec::SmallVec;
24use std::{hash::Hash, sync::Arc};
25
26#[derive(Debug, Clone, Copy, thiserror::Error)]
38#[error("IRI confused with prefix")]
39pub struct IriConfusedWithPrefix;
40
41fn candidate_beats(key: &str, suffix: &str, candidate_len: usize, current: &str) -> bool {
61 use std::cmp::Ordering;
62 match candidate_len.cmp(¤t.len()) {
63 Ordering::Less => true,
64 Ordering::Greater => false,
65 Ordering::Equal => {
66 let cand = key.bytes().chain(std::iter::once(b':')).chain(suffix.bytes());
67 cand.cmp(current.bytes()) == Ordering::Less
68 }
69 }
70}
71
72pub(crate) struct CompactIriMemo<'a, T> {
88 key: Option<MemoKey<'a, T>>,
89 result: Option<Arc<str>>,
90}
91
92struct MemoKey<'a, T> {
93 containers: SmallVec<[Container; 8]>,
94 selection: Selection<'a, T>,
95}
96
97impl<T> CompactIriMemo<'_, T> {
98 pub fn new() -> Self {
99 Self { key: None, result: None }
100 }
101}
102
103impl<'a, T: PartialEq> CompactIriMemo<'a, T> {
104 #[allow(clippy::option_option)]
108 fn get(&self, containers: &[Container], selection: &Selection<'a, T>) -> Option<Option<Arc<str>>> {
109 let key = self.key.as_ref()?;
110 (key.containers.as_slice() == containers && key.selection == *selection).then(|| self.result.clone())
111 }
112
113 fn store(&mut self, containers: SmallVec<[Container; 8]>, selection: Selection<'a, T>, result: Option<Arc<str>>) {
114 self.key = Some(MemoKey { containers, selection });
115 self.result = result;
116 }
117}
118
119pub(crate) fn compact_iri<N>(
129 vocabulary: &N,
130 active_context: &Context<N::Iri, N::BlankId>,
131 var: &Term<N::Iri, N::BlankId>,
132 vocab: bool,
133 reverse: bool,
134 options: Options,
135) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
136where
137 N: Vocabulary,
138 N::Iri: Clone + Hash + Eq,
139 N::BlankId: Clone + Hash + Eq,
140{
141 let cache = active_context.compact_iri_cache();
142 if let Some(hit) = cache.get(&CompactIriKeyRef(var, vocab, reverse, options.processing_mode)) {
143 return Ok(hit);
144 }
145
146 let result = compact_iri_full::<N, Object<N::Iri, N::BlankId>>(vocabulary, active_context, var, None, vocab, reverse, options, None)?;
147
148 cache.insert((var.clone(), vocab, reverse, options.processing_mode), result.clone());
149 Ok(result)
150}
151
152pub(crate) fn keyword_alias<'a, N>(vocabulary: &N, active_context: &'a Context<N::Iri, N::BlankId>, options: Options, k: Keyword) -> &'a str
164where
165 N: Vocabulary,
166 N::Iri: Clone + Hash + Eq,
167 N::BlankId: Clone + Hash + Eq,
168{
169 let aliases = active_context.keyword_aliases_or_init(options.processing_mode, || {
170 let arr: [Box<str>; 13] = std::array::from_fn(|i| {
171 let kw = CACHED_KEYWORDS[i];
172 match compact_iri(vocabulary, active_context, &Term::Keyword(kw), true, false, options).ok().flatten() {
173 Some(arc) => Box::<str>::from(&*arc),
174 None => Box::<str>::from(kw.into_str()),
175 }
176 });
177 KeywordAliases::new(arr)
178 });
179 aliases.get(k)
180}
181
182pub(crate) fn compact_iri_with<N, O>(
187 vocabulary: &N,
188 active_context: &Context<N::Iri, N::BlankId>,
189 var: &Term<N::Iri, N::BlankId>,
190 value: &Indexed<O>,
191 vocab: bool,
192 reverse: bool,
193 options: Options,
194) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
195where
196 N: Vocabulary,
197 N::Iri: Clone + Hash + Eq,
198 N::BlankId: Clone + Hash + Eq,
199 O: object::Any<N::Iri, N::BlankId>,
200{
201 compact_iri_full(vocabulary, active_context, var, Some(value), vocab, reverse, options, None)
202}
203
204pub(crate) fn compact_iri_with_memo<'a, N, O>(
210 vocabulary: &N,
211 active_context: &'a Context<N::Iri, N::BlankId>,
212 var: &Term<N::Iri, N::BlankId>,
213 value: &'a Indexed<O>,
214 vocab: bool,
215 reverse: bool,
216 options: Options,
217 memo: &mut CompactIriMemo<'a, N::Iri>,
218) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
219where
220 N: Vocabulary,
221 N::Iri: Clone + Hash + Eq,
222 N::BlankId: Clone + Hash + Eq,
223 O: object::Any<N::Iri, N::BlankId>,
224{
225 compact_iri_full(vocabulary, active_context, var, Some(value), vocab, reverse, options, Some(memo))
226}
227
228pub(crate) fn compact_iri_full<'a, N, O>(
246 vocabulary: &N,
247 active_context: &'a Context<N::Iri, N::BlankId>,
248 var: &Term<N::Iri, N::BlankId>,
249 value: Option<&'a Indexed<O>>,
250 vocab: bool,
251 reverse: bool,
252 options: Options,
253 mut memo: Option<&mut CompactIriMemo<'a, N::Iri>>,
254) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
255where
256 N: Vocabulary,
257 N::Iri: Clone + Hash + Eq,
258 N::BlankId: Clone + Hash + Eq,
259 O: object::Any<N::Iri, N::BlankId>,
260{
261 if var.is_null() {
262 return Ok(None);
263 }
264
265 if vocab && let Some(entry) = active_context.inverse().get(var) {
266 let mut containers: SmallVec<[Container; 8]> = SmallVec::new();
270 let mut type_lang_value = None;
271
272 if let Some(value) = value
273 && value.index().is_some()
274 && !value.is_graph()
275 {
276 containers.push(Container::Index);
277 containers.push(Container::IndexSet);
278 }
279
280 let mut has_index = false;
281 let mut is_simple_value = false; if reverse {
284 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Reverse));
285 containers.push(Container::Set);
286 } else {
287 let value_ref = value.map(|v| {
288 has_index = v.index().is_some();
289 v.inner().as_ref()
290 });
291
292 match value_ref {
293 Some(object::Ref::List(list)) => {
294 if !has_index {
295 containers.push(Container::List);
296 }
297
298 let mut common_type = None;
299 let mut common_lang_dir = None;
300
301 if list.is_empty() {
302 common_lang_dir = Some(Nullable::Some((active_context.default_language(), active_context.default_base_direction())));
303 } else {
304 for item in list {
305 let mut item_type = None;
306 let mut item_lang_dir = None;
307 let mut is_value = false;
308
309 match item.inner() {
310 Object::Value(value) => {
311 is_value = true;
312 match value {
313 Value::LangString(lang_str) => item_lang_dir = Some(Nullable::Some((lang_str.language(), lang_str.direction()))),
314 Value::Literal(_, Some(ty)) => item_type = Some(Type::Iri(ty.clone())),
315 Value::Literal(_, None) => item_lang_dir = Some(Nullable::Null),
316 Value::Json(_) => item_type = Some(Type::Json),
317 }
318 }
319 _ => item_type = Some(Type::Id),
320 }
321
322 if common_lang_dir.is_none() {
323 common_lang_dir = item_lang_dir;
324 } else if is_value && common_lang_dir != item_lang_dir {
325 common_lang_dir = Some(Nullable::Some((None, None)));
326 }
327
328 if common_type.is_none() {
329 common_type = Some(item_type);
330 } else if common_type.as_ref().is_some_and(|t| *t != item_type) {
331 common_type = Some(None);
332 }
333
334 if common_lang_dir == Some(Nullable::Some((None, None))) && common_type == Some(None) {
335 break;
336 }
337 }
338 }
339
340 let common_lang_dir = common_lang_dir.unwrap_or(Nullable::Some((None, None)));
341 let common_type = common_type.unwrap_or(None);
342
343 if let Some(common_type) = common_type {
344 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(common_type)));
345 } else {
346 type_lang_value = Some(TypeLangValue::Lang(LangSelection::Lang(common_lang_dir)));
347 }
348 }
349 Some(object::Ref::Node(node)) if node.is_graph() => {
350 if has_index {
353 containers.push(Container::GraphIndex);
356 containers.push(Container::GraphIndexSet);
357 }
358
359 if node.id.is_some() {
360 containers.push(Container::GraphId);
363 containers.push(Container::GraphIdSet);
364 }
365
366 containers.push(Container::Graph);
368 containers.push(Container::GraphSet);
369 containers.push(Container::Set);
370
371 if !has_index {
372 containers.push(Container::GraphIndex);
375 containers.push(Container::GraphIndexSet);
376 }
377
378 if node.id.is_none() {
379 containers.push(Container::GraphId);
382 containers.push(Container::GraphIdSet);
383 }
384
385 containers.push(Container::Index);
387 containers.push(Container::IndexSet);
388
389 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(Type::Id)));
390 }
391 Some(object::Ref::Value(v)) => {
392 if (v.direction().is_some() || v.language().is_some()) && !has_index {
394 type_lang_value = Some(TypeLangValue::Lang(LangSelection::Lang(Nullable::Some((v.language(), v.direction())))));
395 containers.push(Container::Language);
396 containers.push(Container::LanguageSet);
397 } else if let Some(ty) = v.typ() {
398 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(ty.as_syntax_type().cloned())));
399 } else {
400 is_simple_value = v.direction().is_none() && v.language().is_none() && !has_index;
401 }
402
403 containers.push(Container::Set);
404 }
405 _ => {
406 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(Type::Id)));
409 containers.push(Container::Id);
410 containers.push(Container::IdSet);
411 containers.push(Container::Type);
412 containers.push(Container::SetType);
413
414 containers.push(Container::Set);
415 }
416 }
417 }
418
419 containers.push(Container::None);
420
421 if options.processing_mode != ProcessingMode::JsonLd1_0 && !has_index {
422 containers.push(Container::Index);
423 containers.push(Container::IndexSet);
424 }
425
426 if options.processing_mode != ProcessingMode::JsonLd1_0 && is_simple_value {
427 containers.push(Container::Language);
428 containers.push(Container::LanguageSet);
429 }
430
431 let mut is_empty_list = false;
432 if let Some(value) = value
433 && let object::Ref::List(list) = value.inner().as_ref()
434 && list.is_empty()
435 {
436 is_empty_list = true;
437 }
438
439 let selection = if is_empty_list {
441 Selection::Any
442 } else {
443 match type_lang_value {
444 Some(TypeLangValue::Type(type_value)) => {
445 let mut selection: Vec<TypeSelection<N::Iri>> = Vec::new();
446
447 if type_value == TypeSelection::Reverse {
448 selection.push(TypeSelection::Reverse);
449 }
450
451 let mut has_id_type = false;
452 if let Some(value) = value
453 && let Some(id) = value.id()
454 && (type_value == TypeSelection::Type(Type::Id) || type_value == TypeSelection::Reverse)
455 {
456 has_id_type = true;
457 let mut vocab = false;
458 if let Some(compacted_iri) = compact_iri(vocabulary, active_context, &id.clone().into_term(), true, false, options)?
459 && let Some(def) = active_context.get(&*compacted_iri)
460 && let Some(iri_mapping) = def.value()
461 {
462 vocab = iri_mapping == id;
463 }
464
465 if vocab {
466 selection.push(TypeSelection::Type(Type::Vocab));
467 selection.push(TypeSelection::Type(Type::Id));
468 } else {
469 selection.push(TypeSelection::Type(Type::Id));
470 selection.push(TypeSelection::Type(Type::Vocab));
471 }
472
473 selection.push(TypeSelection::Type(Type::None));
474 }
475
476 if !has_id_type {
477 selection.push(type_value);
478 selection.push(TypeSelection::Type(Type::None));
479 }
480
481 selection.push(TypeSelection::Any);
482
483 Selection::Type(selection)
484 }
485 Some(TypeLangValue::Lang(lang_value)) => {
486 let mut selection = vec![lang_value, LangSelection::Lang(Nullable::Some((None, None))), LangSelection::Any];
487
488 if let LangSelection::Lang(Nullable::Some((Some(_), Some(dir)))) = lang_value {
489 selection.push(LangSelection::Lang(Nullable::Some((None, Some(dir)))));
490 }
491
492 Selection::Lang(selection)
493 }
494 None => Selection::Lang(vec![
495 LangSelection::Lang(Nullable::Null),
496 LangSelection::Lang(Nullable::Some((None, None))),
497 LangSelection::Any,
498 ]),
499 }
500 };
501
502 if let Some(memo) = memo.as_deref_mut()
503 && let Some(hit) = memo.get(&containers, &selection)
504 {
505 return Ok(hit);
506 }
507
508 let result = match entry.select(&containers, &selection) {
509 Some(term) => Some(Arc::from(term.as_str())),
510 None => compact_iri_fallback(vocabulary, active_context, var, value.is_none(), vocab)?,
513 };
514
515 if let Some(memo) = memo {
516 memo.store(containers, selection, result.clone());
517 }
518
519 return Ok(result);
520 }
521
522 compact_iri_fallback(vocabulary, active_context, var, value.is_none(), vocab)
523}
524
525fn compact_iri_fallback<N>(
538 vocabulary: &N,
539 active_context: &Context<N::Iri, N::BlankId>,
540 var: &Term<N::Iri, N::BlankId>,
541 no_value: bool,
542 vocab: bool,
543) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
544where
545 N: Vocabulary,
546 N::Iri: Clone + Hash + Eq,
547 N::BlankId: Clone + Hash + Eq,
548{
549 if vocab {
550 if let Some(vocab_mapping) = active_context.vocabulary() {
553 if let Some(suffix) = var.with(vocabulary).as_str().strip_prefix(vocab_mapping.with(vocabulary).as_str())
557 && !suffix.is_empty()
558 && active_context.get(suffix).is_none()
559 {
560 return Ok(Some(Arc::from(suffix)));
561 }
562 }
563 }
564
565 let mut compact_iri: Option<String> = None;
569 let var_str = var.with(vocabulary).as_str();
570
571 let mut buf: SmallVec<[u8; 64]> = SmallVec::new();
575
576 for (key, iri_mapping) in active_context.prefix_terms() {
579 let Some(suffix) = var_str.strip_prefix((**iri_mapping).with(vocabulary).as_str()) else {
580 continue;
581 };
582 if suffix.is_empty() {
583 continue;
584 }
585
586 let key_str = key.as_str();
587 let candidate_len = key_str.len() + 1 + suffix.len();
588
589 if let Some(current) = compact_iri.as_deref()
592 && !candidate_beats(key_str, suffix, candidate_len, current)
593 {
594 continue;
595 }
596
597 buf.clear();
600 buf.reserve(candidate_len);
601 buf.extend_from_slice(key_str.as_bytes());
602 buf.push(b':');
603 buf.extend_from_slice(suffix.as_bytes());
604 let candidate_str: &str = unsafe { std::str::from_utf8_unchecked(&buf) };
607
608 let candidate_def = active_context.get(candidate_str);
612 let definition_ok = match candidate_def {
613 None => true,
614 Some(def) => def.value() == Some(var) && no_value,
615 };
616 if definition_ok {
617 compact_iri = Some(candidate_str.to_owned());
618 }
619 }
620
621 if let Some(compact_iri) = compact_iri {
623 return Ok(Some(Arc::from(compact_iri)));
624 }
625
626 if let Some(iri) = var.as_iri()
636 && let Some(iri) = vocabulary.iri(iri)
637 && active_context.contains_term(iri.scheme())
638 {
639 return Err(IriConfusedWithPrefix);
640 }
641
642 if !vocab
645 && let Some(base_iri) = active_context.base_iri()
646 && let Some(iri) = var.as_iri()
647 {
648 let Some(iri) = vocabulary.iri(iri) else {
649 return Ok(None);
650 };
651 let Some(base) = vocabulary.iri(base_iri) else {
652 return Ok(None);
653 };
654 let rel = iri.relative_to(&base);
655 let s = rel.as_str();
656 let out = if s.is_empty() {
662 match base.as_str().rsplit('/').next() {
663 Some(segment) if !segment.is_empty() => segment.to_string(),
664 _ => "./".to_string(),
665 }
666 } else {
667 s.to_string()
668 };
669 return Ok(Some(Arc::from(disambiguate_keyword(out))));
670 }
671
672 Ok(Some(Arc::from(var.with(vocabulary).to_string())))
674}
675
676fn disambiguate_keyword(s: String) -> String {
684 if is_keyword_like(&s) && !is_keyword(&s) { "./".to_string() + &s } else { s }
685}
686
687#[cfg(test)]
688mod tests {
689 #![allow(clippy::unwrap_used)]
690 use super::*;
691 use jsonld_core::{Id, ValidId, context::NormalTermDefinition};
692 use rdfx::{BlankIdBuf, IriBuf, vocabulary::no_vocabulary};
693
694 #[test]
699 fn compact_iri_cache_distinguishes_processing_modes() {
700 let iri = IriBuf::new("http://example.com/t".to_string()).unwrap();
701 let term = Term::<IriBuf, BlankIdBuf>::Id(Id::Valid(ValidId::Iri(iri)));
702
703 let mut context: Context<IriBuf, BlankIdBuf> = Context::new(None);
704 context.set_normal(
705 "t".into(),
706 Some(NormalTermDefinition {
707 value: Some(Arc::new(term.clone())),
708 container: Container::Index,
709 ..Default::default()
710 }),
711 );
712
713 let options_1_1 = Options {
714 processing_mode: ProcessingMode::JsonLd1_1,
715 ..Default::default()
716 };
717 let options_1_0 = Options {
718 processing_mode: ProcessingMode::JsonLd1_0,
719 ..Default::default()
720 };
721
722 let compacted_1_1 = compact_iri(no_vocabulary(), &context, &term, true, false, options_1_1).unwrap();
723 let compacted_1_0 = compact_iri(no_vocabulary(), &context, &term, true, false, options_1_0).unwrap();
724
725 assert_eq!(compacted_1_1.as_deref(), Some("t"));
726 assert_eq!(compacted_1_0.as_deref(), Some("http://example.com/t"));
727 }
728
729 #[test]
732 fn relativization_of_base_with_trailing_slash_is_not_empty() {
733 let base = IriBuf::new("http://example.com/dir/".to_string()).unwrap();
734 let iri = IriBuf::new("http://example.com/dir/".to_string()).unwrap();
735 let term = Term::<IriBuf, BlankIdBuf>::Id(Id::Valid(ValidId::Iri(iri)));
736
737 let mut context: Context<IriBuf, BlankIdBuf> = Context::new(None);
738 context.set_base_iri(Some(base));
739
740 let compacted = compact_iri(no_vocabulary(), &context, &term, false, false, Options::default()).unwrap();
741 assert_eq!(compacted.as_deref(), Some("./"));
742 }
743}