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 {
143 let guard = cache.lock();
144 if let Some(hit) = guard.get(&CompactIriKeyRef(var, vocab, reverse, options.processing_mode)) {
145 return Ok(hit.clone());
146 }
147 }
148
149 let result = compact_iri_full::<N, Object<N::Iri, N::BlankId>>(vocabulary, active_context, var, None, vocab, reverse, options, None)?;
150
151 cache.lock().insert((var.clone(), vocab, reverse, options.processing_mode), result.clone());
152 Ok(result)
153}
154
155pub(crate) fn keyword_alias<'a, N>(vocabulary: &N, active_context: &'a Context<N::Iri, N::BlankId>, options: Options, k: Keyword) -> &'a str
167where
168 N: Vocabulary,
169 N::Iri: Clone + Hash + Eq,
170 N::BlankId: Clone + Hash + Eq,
171{
172 let aliases = active_context.keyword_aliases_or_init(options.processing_mode, || {
173 let arr: [Box<str>; 13] = std::array::from_fn(|i| {
174 let kw = CACHED_KEYWORDS[i];
175 match compact_iri(vocabulary, active_context, &Term::Keyword(kw), true, false, options).ok().flatten() {
176 Some(arc) => Box::<str>::from(&*arc),
177 None => Box::<str>::from(kw.into_str()),
178 }
179 });
180 KeywordAliases::new(arr)
181 });
182 aliases.get(k)
183}
184
185pub(crate) fn compact_iri_with<N, O>(
190 vocabulary: &N,
191 active_context: &Context<N::Iri, N::BlankId>,
192 var: &Term<N::Iri, N::BlankId>,
193 value: &Indexed<O>,
194 vocab: bool,
195 reverse: bool,
196 options: Options,
197) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
198where
199 N: Vocabulary,
200 N::Iri: Clone + Hash + Eq,
201 N::BlankId: Clone + Hash + Eq,
202 O: object::Any<N::Iri, N::BlankId>,
203{
204 compact_iri_full(vocabulary, active_context, var, Some(value), vocab, reverse, options, None)
205}
206
207pub(crate) fn compact_iri_with_memo<'a, N, O>(
213 vocabulary: &N,
214 active_context: &'a Context<N::Iri, N::BlankId>,
215 var: &Term<N::Iri, N::BlankId>,
216 value: &'a Indexed<O>,
217 vocab: bool,
218 reverse: bool,
219 options: Options,
220 memo: &mut CompactIriMemo<'a, N::Iri>,
221) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
222where
223 N: Vocabulary,
224 N::Iri: Clone + Hash + Eq,
225 N::BlankId: Clone + Hash + Eq,
226 O: object::Any<N::Iri, N::BlankId>,
227{
228 compact_iri_full(vocabulary, active_context, var, Some(value), vocab, reverse, options, Some(memo))
229}
230
231pub(crate) fn compact_iri_full<'a, N, O>(
249 vocabulary: &N,
250 active_context: &'a Context<N::Iri, N::BlankId>,
251 var: &Term<N::Iri, N::BlankId>,
252 value: Option<&'a Indexed<O>>,
253 vocab: bool,
254 reverse: bool,
255 options: Options,
256 mut memo: Option<&mut CompactIriMemo<'a, N::Iri>>,
257) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
258where
259 N: Vocabulary,
260 N::Iri: Clone + Hash + Eq,
261 N::BlankId: Clone + Hash + Eq,
262 O: object::Any<N::Iri, N::BlankId>,
263{
264 if var.is_null() {
265 return Ok(None);
266 }
267
268 if vocab && let Some(entry) = active_context.inverse().get(var) {
269 let mut containers: SmallVec<[Container; 8]> = SmallVec::new();
273 let mut type_lang_value = None;
274
275 if let Some(value) = value
276 && value.index().is_some()
277 && !value.is_graph()
278 {
279 containers.push(Container::Index);
280 containers.push(Container::IndexSet);
281 }
282
283 let mut has_index = false;
284 let mut is_simple_value = false; if reverse {
287 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Reverse));
288 containers.push(Container::Set);
289 } else {
290 let value_ref = value.map(|v| {
291 has_index = v.index().is_some();
292 v.inner().as_ref()
293 });
294
295 match value_ref {
296 Some(object::Ref::List(list)) => {
297 if !has_index {
298 containers.push(Container::List);
299 }
300
301 let mut common_type = None;
302 let mut common_lang_dir = None;
303
304 if list.is_empty() {
305 common_lang_dir = Some(Nullable::Some((active_context.default_language(), active_context.default_base_direction())));
306 } else {
307 for item in list {
308 let mut item_type = None;
309 let mut item_lang_dir = None;
310 let mut is_value = false;
311
312 match item.inner() {
313 Object::Value(value) => {
314 is_value = true;
315 match value {
316 Value::LangString(lang_str) => item_lang_dir = Some(Nullable::Some((lang_str.language(), lang_str.direction()))),
317 Value::Literal(_, Some(ty)) => item_type = Some(Type::Iri(ty.clone())),
318 Value::Literal(_, None) => item_lang_dir = Some(Nullable::Null),
319 Value::Json(_) => item_type = Some(Type::Json),
320 }
321 }
322 _ => item_type = Some(Type::Id),
323 }
324
325 if common_lang_dir.is_none() {
326 common_lang_dir = item_lang_dir;
327 } else if is_value && common_lang_dir != item_lang_dir {
328 common_lang_dir = Some(Nullable::Some((None, None)));
329 }
330
331 if common_type.is_none() {
332 common_type = Some(item_type);
333 } else if common_type.as_ref().is_some_and(|t| *t != item_type) {
334 common_type = Some(None);
335 }
336
337 if common_lang_dir == Some(Nullable::Some((None, None))) && common_type == Some(None) {
338 break;
339 }
340 }
341 }
342
343 let common_lang_dir = common_lang_dir.unwrap_or(Nullable::Some((None, None)));
344 let common_type = common_type.unwrap_or(None);
345
346 if let Some(common_type) = common_type {
347 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(common_type)));
348 } else {
349 type_lang_value = Some(TypeLangValue::Lang(LangSelection::Lang(common_lang_dir)));
350 }
351 }
352 Some(object::Ref::Node(node)) if node.is_graph() => {
353 if has_index {
356 containers.push(Container::GraphIndex);
359 containers.push(Container::GraphIndexSet);
360 }
361
362 if node.id.is_some() {
363 containers.push(Container::GraphId);
366 containers.push(Container::GraphIdSet);
367 }
368
369 containers.push(Container::Graph);
371 containers.push(Container::GraphSet);
372 containers.push(Container::Set);
373
374 if !has_index {
375 containers.push(Container::GraphIndex);
378 containers.push(Container::GraphIndexSet);
379 }
380
381 if node.id.is_none() {
382 containers.push(Container::GraphId);
385 containers.push(Container::GraphIdSet);
386 }
387
388 containers.push(Container::Index);
390 containers.push(Container::IndexSet);
391
392 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(Type::Id)));
393 }
394 Some(object::Ref::Value(v)) => {
395 if (v.direction().is_some() || v.language().is_some()) && !has_index {
397 type_lang_value = Some(TypeLangValue::Lang(LangSelection::Lang(Nullable::Some((v.language(), v.direction())))));
398 containers.push(Container::Language);
399 containers.push(Container::LanguageSet);
400 } else if let Some(ty) = v.typ() {
401 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(ty.as_syntax_type().cloned())));
402 } else {
403 is_simple_value = v.direction().is_none() && v.language().is_none() && !has_index;
404 }
405
406 containers.push(Container::Set);
407 }
408 _ => {
409 type_lang_value = Some(TypeLangValue::Type(TypeSelection::Type(Type::Id)));
412 containers.push(Container::Id);
413 containers.push(Container::IdSet);
414 containers.push(Container::Type);
415 containers.push(Container::SetType);
416
417 containers.push(Container::Set);
418 }
419 }
420 }
421
422 containers.push(Container::None);
423
424 if options.processing_mode != ProcessingMode::JsonLd1_0 && !has_index {
425 containers.push(Container::Index);
426 containers.push(Container::IndexSet);
427 }
428
429 if options.processing_mode != ProcessingMode::JsonLd1_0 && is_simple_value {
430 containers.push(Container::Language);
431 containers.push(Container::LanguageSet);
432 }
433
434 let mut is_empty_list = false;
435 if let Some(value) = value
436 && let object::Ref::List(list) = value.inner().as_ref()
437 && list.is_empty()
438 {
439 is_empty_list = true;
440 }
441
442 let selection = if is_empty_list {
444 Selection::Any
445 } else {
446 match type_lang_value {
447 Some(TypeLangValue::Type(type_value)) => {
448 let mut selection: Vec<TypeSelection<N::Iri>> = Vec::new();
449
450 if type_value == TypeSelection::Reverse {
451 selection.push(TypeSelection::Reverse);
452 }
453
454 let mut has_id_type = false;
455 if let Some(value) = value
456 && let Some(id) = value.id()
457 && (type_value == TypeSelection::Type(Type::Id) || type_value == TypeSelection::Reverse)
458 {
459 has_id_type = true;
460 let mut vocab = false;
461 if let Some(compacted_iri) = compact_iri(vocabulary, active_context, &id.clone().into_term(), true, false, options)?
462 && let Some(def) = active_context.get(&*compacted_iri)
463 && let Some(iri_mapping) = def.value()
464 {
465 vocab = iri_mapping == id;
466 }
467
468 if vocab {
469 selection.push(TypeSelection::Type(Type::Vocab));
470 selection.push(TypeSelection::Type(Type::Id));
471 } else {
472 selection.push(TypeSelection::Type(Type::Id));
473 selection.push(TypeSelection::Type(Type::Vocab));
474 }
475
476 selection.push(TypeSelection::Type(Type::None));
477 }
478
479 if !has_id_type {
480 selection.push(type_value);
481 selection.push(TypeSelection::Type(Type::None));
482 }
483
484 selection.push(TypeSelection::Any);
485
486 Selection::Type(selection)
487 }
488 Some(TypeLangValue::Lang(lang_value)) => {
489 let mut selection = vec![lang_value, LangSelection::Lang(Nullable::Some((None, None))), LangSelection::Any];
490
491 if let LangSelection::Lang(Nullable::Some((Some(_), Some(dir)))) = lang_value {
492 selection.push(LangSelection::Lang(Nullable::Some((None, Some(dir)))));
493 }
494
495 Selection::Lang(selection)
496 }
497 None => Selection::Lang(vec![
498 LangSelection::Lang(Nullable::Null),
499 LangSelection::Lang(Nullable::Some((None, None))),
500 LangSelection::Any,
501 ]),
502 }
503 };
504
505 if let Some(memo) = memo.as_deref_mut()
506 && let Some(hit) = memo.get(&containers, &selection)
507 {
508 return Ok(hit);
509 }
510
511 let result = match entry.select(&containers, &selection) {
512 Some(term) => Some(Arc::from(term.as_str())),
513 None => compact_iri_fallback(vocabulary, active_context, var, value.is_none(), vocab)?,
516 };
517
518 if let Some(memo) = memo {
519 memo.store(containers, selection, result.clone());
520 }
521
522 return Ok(result);
523 }
524
525 compact_iri_fallback(vocabulary, active_context, var, value.is_none(), vocab)
526}
527
528fn compact_iri_fallback<N>(
541 vocabulary: &N,
542 active_context: &Context<N::Iri, N::BlankId>,
543 var: &Term<N::Iri, N::BlankId>,
544 no_value: bool,
545 vocab: bool,
546) -> Result<Option<Arc<str>>, IriConfusedWithPrefix>
547where
548 N: Vocabulary,
549 N::Iri: Clone + Hash + Eq,
550 N::BlankId: Clone + Hash + Eq,
551{
552 if vocab {
553 if let Some(vocab_mapping) = active_context.vocabulary() {
556 if let Some(suffix) = var.with(vocabulary).as_str().strip_prefix(vocab_mapping.with(vocabulary).as_str())
560 && !suffix.is_empty()
561 && active_context.get(suffix).is_none()
562 {
563 return Ok(Some(Arc::from(suffix)));
564 }
565 }
566 }
567
568 let mut compact_iri: Option<String> = None;
572 let var_str = var.with(vocabulary).as_str();
573
574 let mut buf: SmallVec<[u8; 64]> = SmallVec::new();
578
579 for (key, iri_mapping) in active_context.prefix_terms() {
582 let Some(suffix) = var_str.strip_prefix((**iri_mapping).with(vocabulary).as_str()) else {
583 continue;
584 };
585 if suffix.is_empty() {
586 continue;
587 }
588
589 let key_str = key.as_str();
590 let candidate_len = key_str.len() + 1 + suffix.len();
591
592 if let Some(current) = compact_iri.as_deref()
595 && !candidate_beats(key_str, suffix, candidate_len, current)
596 {
597 continue;
598 }
599
600 buf.clear();
603 buf.reserve(candidate_len);
604 buf.extend_from_slice(key_str.as_bytes());
605 buf.push(b':');
606 buf.extend_from_slice(suffix.as_bytes());
607 let candidate_str: &str = unsafe { std::str::from_utf8_unchecked(&buf) };
610
611 let candidate_def = active_context.get(candidate_str);
615 let definition_ok = match candidate_def {
616 None => true,
617 Some(def) => def.value() == Some(var) && no_value,
618 };
619 if definition_ok {
620 compact_iri = Some(candidate_str.to_owned());
621 }
622 }
623
624 if let Some(compact_iri) = compact_iri {
626 return Ok(Some(Arc::from(compact_iri)));
627 }
628
629 if let Some(iri) = var.as_iri()
639 && let Some(iri) = vocabulary.iri(iri)
640 && active_context.contains_term(iri.scheme())
641 {
642 return Err(IriConfusedWithPrefix);
643 }
644
645 if !vocab
648 && let Some(base_iri) = active_context.base_iri()
649 && let Some(iri) = var.as_iri()
650 {
651 let Some(iri) = vocabulary.iri(iri) else {
652 return Ok(None);
653 };
654 let Some(base) = vocabulary.iri(base_iri) else {
655 return Ok(None);
656 };
657 let rel = iri.relative_to(&base);
658 let s = rel.as_str();
659 let out = if s.is_empty() {
665 match base.as_str().rsplit('/').next() {
666 Some(segment) if !segment.is_empty() => segment.to_string(),
667 _ => "./".to_string(),
668 }
669 } else {
670 s.to_string()
671 };
672 return Ok(Some(Arc::from(disambiguate_keyword(out))));
673 }
674
675 Ok(Some(Arc::from(var.with(vocabulary).to_string())))
677}
678
679fn disambiguate_keyword(s: String) -> String {
687 if is_keyword_like(&s) && !is_keyword(&s) { "./".to_string() + &s } else { s }
688}
689
690#[cfg(test)]
691mod tests {
692 #![allow(clippy::unwrap_used)]
693 use super::*;
694 use jsonld_core::{Id, ValidId, context::NormalTermDefinition};
695 use rdfx::{BlankIdBuf, IriBuf, vocabulary::no_vocabulary};
696
697 #[test]
702 fn compact_iri_cache_distinguishes_processing_modes() {
703 let iri = IriBuf::new("http://example.com/t".to_string()).unwrap();
704 let term = Term::<IriBuf, BlankIdBuf>::Id(Id::Valid(ValidId::Iri(iri)));
705
706 let mut context: Context<IriBuf, BlankIdBuf> = Context::new(None);
707 context.set_normal(
708 "t".into(),
709 Some(NormalTermDefinition {
710 value: Some(Arc::new(term.clone())),
711 container: Container::Index,
712 ..Default::default()
713 }),
714 );
715
716 let options_1_1 = Options {
717 processing_mode: ProcessingMode::JsonLd1_1,
718 ..Default::default()
719 };
720 let options_1_0 = Options {
721 processing_mode: ProcessingMode::JsonLd1_0,
722 ..Default::default()
723 };
724
725 let compacted_1_1 = compact_iri(no_vocabulary(), &context, &term, true, false, options_1_1).unwrap();
726 let compacted_1_0 = compact_iri(no_vocabulary(), &context, &term, true, false, options_1_0).unwrap();
727
728 assert_eq!(compacted_1_1.as_deref(), Some("t"));
729 assert_eq!(compacted_1_0.as_deref(), Some("http://example.com/t"));
730 }
731
732 #[test]
735 fn relativization_of_base_with_trailing_slash_is_not_empty() {
736 let base = IriBuf::new("http://example.com/dir/".to_string()).unwrap();
737 let iri = IriBuf::new("http://example.com/dir/".to_string()).unwrap();
738 let term = Term::<IriBuf, BlankIdBuf>::Id(Id::Valid(ValidId::Iri(iri)));
739
740 let mut context: Context<IriBuf, BlankIdBuf> = Context::new(None);
741 context.set_base_iri(Some(base));
742
743 let compacted = compact_iri(no_vocabulary(), &context, &term, false, false, Options::default()).unwrap();
744 assert_eq!(compacted.as_deref(), Some("./"));
745 }
746}