1use derive_where::derive_where;
2use jsonld_core::{Context, Direction, Id, Indexed, LenientLangTag, Nullable, Object, Type, Value, object};
3use std::fmt;
4
5#[derive_where(Clone, Copy)]
23#[derive_where(Debug; T: fmt::Debug, B: fmt::Debug)]
24pub struct ValueHint<'a, T, B> {
25 pub kind: ValueKind<'a, T, B>,
27
28 pub indexed: bool,
30}
31
32impl<'a, T, B> ValueHint<'a, T, B> {
33 #[must_use]
35 pub const fn new(kind: ValueKind<'a, T, B>) -> Self {
36 Self { kind, indexed: false }
37 }
38
39 #[must_use]
41 pub const fn indexed(kind: ValueKind<'a, T, B>) -> Self {
42 Self { kind, indexed: true }
43 }
44}
45
46#[derive_where(Clone, Copy)]
51#[derive_where(Debug; T: fmt::Debug, B: fmt::Debug)]
52pub enum ValueKind<'a, T, B> {
53 Node {
60 id: Option<&'a Id<T, B>>,
62 },
63
64 Graph {
67 id: Option<&'a Id<T, B>>,
69 },
70
71 Literal {
74 datatype: Option<&'a T>,
76 },
77
78 LangString {
81 language: Option<&'a LenientLangTag>,
83
84 direction: Option<Direction>,
86 },
87
88 Json,
90
91 List(&'a [ValueKind<'a, T, B>]),
97
98 EmptyArray,
102}
103
104impl<'a, T, B> ValueKind<'a, T, B> {
105 fn value_object(self) -> Option<ValueObjectFeatures<'a, T>> {
108 match self {
109 Self::Literal { datatype } => Some(ValueObjectFeatures::Literal(datatype)),
110 Self::LangString { language, direction } => Some(ValueObjectFeatures::LangString { language, direction }),
111 Self::Json => Some(ValueObjectFeatures::Json),
112 Self::Node { .. } | Self::Graph { .. } | Self::List(_) | Self::EmptyArray => None,
113 }
114 }
115}
116
117pub(crate) struct ValueFeatures<'a, T, B> {
122 pub(crate) has_index: bool,
124
125 pub(crate) shape: ValueShape<'a, T, B>,
127}
128
129impl<'a, T, B> ValueFeatures<'a, T, B> {
130 pub(crate) fn id(&self) -> Option<&'a Id<T, B>> {
133 match &self.shape {
134 ValueShape::Node { id } | ValueShape::Graph { id } => *id,
135 ValueShape::Value(_) | ValueShape::List { .. } => None,
136 }
137 }
138
139 pub(crate) fn is_graph(&self) -> bool {
141 matches!(self.shape, ValueShape::Graph { .. })
142 }
143
144 pub(crate) fn is_empty_list(&self) -> bool {
146 matches!(self.shape, ValueShape::List { is_empty: true, .. })
147 }
148}
149
150pub(crate) enum ValueShape<'a, T, B> {
152 Node { id: Option<&'a Id<T, B>> },
154
155 Graph { id: Option<&'a Id<T, B>> },
157
158 Value(ValueObjectFeatures<'a, T>),
160
161 List {
164 common_type: Option<Type<T>>,
165 common_lang_dir: Nullable<(Option<&'a LenientLangTag>, Option<Direction>)>,
166 is_empty: bool,
167 },
168}
169
170pub(crate) enum ValueObjectFeatures<'a, T> {
172 Literal(Option<&'a T>),
174
175 LangString {
177 language: Option<&'a LenientLangTag>,
178 direction: Option<Direction>,
179 },
180
181 Json,
183}
184
185impl<'a, T> ValueObjectFeatures<'a, T> {
186 fn of_value(value: &'a Value<T>) -> Self {
188 match value {
189 Value::Literal(_, datatype) => Self::Literal(datatype.as_ref()),
190 Value::LangString(s) => Self::LangString {
191 language: s.language(),
192 direction: s.direction(),
193 },
194 Value::Json(_) => Self::Json,
195 }
196 }
197
198 pub(crate) fn language(&self) -> Option<&'a LenientLangTag> {
200 match self {
201 Self::LangString { language, .. } => *language,
202 Self::Literal(_) | Self::Json => None,
203 }
204 }
205
206 pub(crate) fn direction(&self) -> Option<Direction> {
208 match self {
209 Self::LangString { direction, .. } => *direction,
210 Self::Literal(_) | Self::Json => None,
211 }
212 }
213
214 pub(crate) fn typ(&self) -> Option<Type<&'a T>> {
216 match self {
217 Self::Literal(Some(datatype)) => Some(Type::Iri(*datatype)),
218 Self::Json => Some(Type::Json),
219 Self::Literal(None) | Self::LangString { .. } => None,
220 }
221 }
222}
223
224pub(crate) trait IntoValueFeatures<'a, T, B> {
226 fn into_value_features(self, active_context: &'a Context<T, B>) -> ValueFeatures<'a, T, B>;
228}
229
230impl<'a, T: Clone + PartialEq, B, O: object::Any<T, B>> IntoValueFeatures<'a, T, B> for &'a Indexed<O> {
231 fn into_value_features(self, active_context: &'a Context<T, B>) -> ValueFeatures<'a, T, B> {
232 let shape = match self.inner().as_ref() {
233 object::Ref::List(list) => list_shape(
234 list.iter().map(|item| match item.inner() {
235 Object::Value(value) => Some(ValueObjectFeatures::of_value(value)),
236 Object::Node(_) | Object::List(_) => None,
237 }),
238 list.is_empty(),
239 active_context,
240 ),
241 object::Ref::Node(node) if node.is_graph() => ValueShape::Graph { id: node.id.as_ref() },
242 object::Ref::Node(node) => ValueShape::Node { id: node.id.as_ref() },
243 object::Ref::Value(value) => ValueShape::Value(ValueObjectFeatures::of_value(value)),
244 };
245
246 ValueFeatures {
247 has_index: self.index().is_some(),
248 shape,
249 }
250 }
251}
252
253impl<'a, T: Clone + PartialEq, B> IntoValueFeatures<'a, T, B> for ValueHint<'a, T, B> {
254 fn into_value_features(self, active_context: &'a Context<T, B>) -> ValueFeatures<'a, T, B> {
255 let shape = match self.kind {
256 ValueKind::Node { id } => ValueShape::Node { id },
257 ValueKind::EmptyArray => ValueShape::Node { id: None },
258 ValueKind::Graph { id } => ValueShape::Graph { id },
259 ValueKind::Literal { datatype } => ValueShape::Value(ValueObjectFeatures::Literal(datatype)),
260 ValueKind::LangString { language, direction } => ValueShape::Value(ValueObjectFeatures::LangString { language, direction }),
261 ValueKind::Json => ValueShape::Value(ValueObjectFeatures::Json),
262 ValueKind::List(items) => list_shape(items.iter().map(|item| item.value_object()), items.is_empty(), active_context),
263 };
264
265 ValueFeatures {
266 has_index: self.indexed,
267 shape,
268 }
269 }
270}
271
272fn list_shape<'a, T: Clone + PartialEq, B>(
282 items: impl Iterator<Item = Option<ValueObjectFeatures<'a, T>>>,
283 is_empty: bool,
284 active_context: &'a Context<T, B>,
285) -> ValueShape<'a, T, B> {
286 let mut common_type = None;
287 let mut common_lang_dir = None;
288
289 if is_empty {
290 common_lang_dir = Some(Nullable::Some((active_context.default_language(), active_context.default_base_direction())));
291 } else {
292 for item in items {
293 let mut item_type = None;
294 let mut item_lang_dir = None;
295 let is_value = item.is_some();
296
297 match item {
298 Some(ValueObjectFeatures::LangString { language, direction }) => item_lang_dir = Some(Nullable::Some((language, direction))),
299 Some(ValueObjectFeatures::Literal(Some(datatype))) => item_type = Some(Type::Iri(datatype.clone())),
300 Some(ValueObjectFeatures::Literal(None)) => item_lang_dir = Some(Nullable::Null),
301 Some(ValueObjectFeatures::Json) => item_type = Some(Type::Json),
302 None => item_type = Some(Type::Id),
303 }
304
305 if common_lang_dir.is_none() {
306 common_lang_dir = item_lang_dir;
307 } else if is_value && common_lang_dir != item_lang_dir {
308 common_lang_dir = Some(Nullable::Some((None, None)));
309 }
310
311 if common_type.is_none() {
312 common_type = Some(item_type);
313 } else if common_type.as_ref().is_some_and(|t| *t != item_type) {
314 common_type = Some(None);
315 }
316
317 if common_lang_dir == Some(Nullable::Some((None, None))) && common_type == Some(None) {
318 break;
319 }
320 }
321 }
322
323 ValueShape::List {
324 common_type: common_type.unwrap_or(None),
325 common_lang_dir: common_lang_dir.unwrap_or(Nullable::Some((None, None))),
326 is_empty,
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 #![allow(clippy::unwrap_used)]
333 use super::*;
334 use crate::{
335 Options,
336 compact_iri_with,
337 compact_iri_with_hint,
338 test_fixtures::{TestContext, TestObject, expanded_object, iri, iri_term, processed_context},
339 };
340 use jsonld_core::{Node, ProcessingMode};
341 use rdfx::{BlankIdBuf, IriBuf, vocabulary::no_vocabulary};
342 use std::collections::BTreeSet;
343
344 const MAPPING_CONTEXT: &str = r#"{
348 "@vocab": "https://vocab.example/",
349 "ex": "https://example.com/",
350 "xsd": "http://www.w3.org/2001/XMLSchema#",
351 "Building": "https://example.com/Building",
352 "name": "https://example.com/name",
353 "nameEn": { "@id": "https://example.com/name", "@language": "en" },
354 "nameArRtl": { "@id": "https://example.com/name", "@language": "ar", "@direction": "rtl" },
355 "nameLtr": { "@id": "https://example.com/name", "@direction": "ltr" },
356 "nameCount": { "@id": "https://example.com/name", "@type": "xsd:integer" },
357 "nameRef": { "@id": "https://example.com/name", "@type": "@id" },
358 "nameVocab": { "@id": "https://example.com/name", "@type": "@vocab" },
359 "nameJson": { "@id": "https://example.com/name", "@type": "@json" },
360 "nameReverse": { "@reverse": "https://example.com/name" }
361 }"#;
362
363 const SELECTION_CONTEXT: &str = r#"{
367 "@vocab": "https://vocab.example/",
368 "ex": "https://example.com/",
369 "xsd": "http://www.w3.org/2001/XMLSchema#",
370 "Building": "https://example.com/Building",
371 "name": "https://example.com/name",
372 "nameEn": { "@id": "https://example.com/name", "@language": "en" },
373 "nameArRtl": { "@id": "https://example.com/name", "@language": "ar", "@direction": "rtl" },
374 "nameLtr": { "@id": "https://example.com/name", "@direction": "ltr" },
375 "nameCount": { "@id": "https://example.com/name", "@type": "xsd:integer" },
376 "nameRef": { "@id": "https://example.com/name", "@type": "@id" },
377 "nameVocab": { "@id": "https://example.com/name", "@type": "@vocab" },
378 "nameJson": { "@id": "https://example.com/name", "@type": "@json" },
379 "nameList": { "@id": "https://example.com/name", "@container": "@list" },
380 "nameSet": { "@id": "https://example.com/name", "@container": "@set" },
381 "nameMap": { "@id": "https://example.com/name", "@container": "@language" },
382 "nameIndex": { "@id": "https://example.com/name", "@container": "@index" },
383 "nameById": { "@id": "https://example.com/name", "@container": "@id" },
384 "nameGraph": { "@id": "https://example.com/name", "@container": "@graph" },
385 "nameNamedGraphs": { "@id": "https://example.com/name", "@container": ["@graph", "@id"] },
386 "nameReverse": { "@reverse": "https://example.com/name" }
387 }"#;
388
389 const PROPERTIES: [&str; 3] = ["https://example.com/name", "https://example.com/unlisted", "https://vocab.example/temperature"];
392
393 const XSD_INTEGER: &str = "http://www.w3.org/2001/XMLSchema#integer";
394
395 fn assert_same_selection(context: &TestContext, object: &TestObject, hint: ValueHint<IriBuf, BlankIdBuf>) -> Option<String> {
400 let modes = [ProcessingMode::JsonLd1_1, ProcessingMode::JsonLd1_0];
401 for processing_mode in modes {
402 let options = Options {
403 processing_mode,
404 ..Options::default()
405 };
406 for property in PROPERTIES {
407 let var = iri_term(property);
408 for reverse in [false, true] {
409 let from_object = compact_iri_with(no_vocabulary(), context, &var, object, true, reverse, options).map_err(|_| ());
410 let from_hint = compact_iri_with_hint(no_vocabulary(), context, &var, hint, true, reverse, options).map_err(|_| ());
411 assert_eq!(
412 from_object, from_hint,
413 "{object:?} and {hint:?} select different terms for {property} (reverse: {reverse}, {processing_mode:?})"
414 );
415 }
416 }
417 }
418
419 compact_iri_with_hint(no_vocabulary(), context, &iri_term(PROPERTIES[0]), hint, true, false, Options::default())
420 .unwrap()
421 .map(|term| term.to_string())
422 }
423
424 #[tokio::test]
425 async fn hint_selects_the_term_its_object_selects() {
426 let contexts = [processed_context(MAPPING_CONTEXT).await, processed_context(SELECTION_CONTEXT).await];
427
428 let building = Id::iri(iri("https://example.com/Building"));
429 let thing = Id::iri(iri("urn:ngsi-ld:Thing:1"));
430 let blank = Id::blank(BlankIdBuf::new("_:b0".to_owned()).unwrap());
431 let graph = Id::iri(iri("urn:graph"));
432 let xsd_integer = iri(XSD_INTEGER);
433 let xsd_date = iri("http://www.w3.org/2001/XMLSchema#date");
434 let en = LenientLangTag::new("en").0;
435 let ar = LenientLangTag::new("ar").0;
436
437 let plain = ValueKind::Literal { datatype: None };
438 let integer = ValueKind::Literal { datatype: Some(&xsd_integer) };
439 let english = ValueKind::LangString {
440 language: Some(en),
441 direction: None,
442 };
443 let plain_items = [plain, plain];
444 let english_items = [english, english];
445 let mixed_items = [integer, english, plain];
446 let reference_items = [ValueKind::Node { id: Some(&thing) }, ValueKind::Node { id: Some(&building) }];
447 let integer_items = [integer, integer];
448 let json_items = [ValueKind::Json];
449
450 let cases: Vec<(&str, ValueHint<IriBuf, BlankIdBuf>)> = vec![
451 (
452 r#"{ "@id": "https://example.com/Building" }"#,
453 ValueHint::new(ValueKind::Node { id: Some(&building) }),
454 ),
455 (r#"{ "@id": "urn:ngsi-ld:Thing:1" }"#, ValueHint::new(ValueKind::Node { id: Some(&thing) })),
456 (r#"{ "@id": "_:b0" }"#, ValueHint::new(ValueKind::Node { id: Some(&blank) })),
457 (
458 r#"{ "@id": "urn:ngsi-ld:Thing:1", "@index": "i" }"#,
459 ValueHint::indexed(ValueKind::Node { id: Some(&thing) }),
460 ),
461 (
462 r#"{ "https://example.com/p": [{ "@value": "x" }] }"#,
463 ValueHint::new(ValueKind::Node { id: None }),
464 ),
465 (
466 r#"{ "@id": "urn:graph", "@graph": [{ "@id": "urn:ngsi-ld:Thing:1" }] }"#,
467 ValueHint::new(ValueKind::Graph { id: Some(&graph) }),
468 ),
469 (
470 r#"{ "@graph": [{ "@id": "urn:ngsi-ld:Thing:1" }], "@index": "i" }"#,
471 ValueHint::indexed(ValueKind::Graph { id: None }),
472 ),
473 (
474 r#"{ "@value": "5", "@type": "http://www.w3.org/2001/XMLSchema#integer" }"#,
475 ValueHint::new(integer),
476 ),
477 (
478 r#"{ "@value": "2024-01-01", "@type": "http://www.w3.org/2001/XMLSchema#date" }"#,
479 ValueHint::new(ValueKind::Literal { datatype: Some(&xsd_date) }),
480 ),
481 (r#"{ "@value": "x" }"#, ValueHint::new(plain)),
482 (r#"{ "@value": 5 }"#, ValueHint::new(plain)),
483 (r#"{ "@value": true }"#, ValueHint::new(plain)),
484 (r#"{ "@value": "x", "@index": "i" }"#, ValueHint::indexed(plain)),
485 (r#"{ "@value": "x", "@language": "en" }"#, ValueHint::new(english)),
486 (
487 r#"{ "@value": "x", "@language": "ar", "@direction": "rtl" }"#,
488 ValueHint::new(ValueKind::LangString {
489 language: Some(ar),
490 direction: Some(Direction::Rtl),
491 }),
492 ),
493 (
494 r#"{ "@value": "x", "@direction": "ltr" }"#,
495 ValueHint::new(ValueKind::LangString {
496 language: None,
497 direction: Some(Direction::Ltr),
498 }),
499 ),
500 (r#"{ "@value": "x", "@language": "en", "@index": "i" }"#, ValueHint::indexed(english)),
501 (r#"{ "@value": { "a": [1, 2.50] }, "@type": "@json" }"#, ValueHint::new(ValueKind::Json)),
502 (
503 r#"{ "@list": [{ "@value": "a" }, { "@value": "b" }] }"#,
504 ValueHint::new(ValueKind::List(&plain_items)),
505 ),
506 (
507 r#"{ "@list": [{ "@value": "a", "@language": "en" }, { "@value": "b", "@language": "en" }] }"#,
508 ValueHint::new(ValueKind::List(&english_items)),
509 ),
510 (
511 r#"{ "@list": [{ "@value": "5", "@type": "http://www.w3.org/2001/XMLSchema#integer" }, { "@value": "a", "@language": "en" }, { "@value": "b" }] }"#,
512 ValueHint::new(ValueKind::List(&mixed_items)),
513 ),
514 (
515 r#"{ "@list": [{ "@id": "urn:ngsi-ld:Thing:1" }, { "@id": "https://example.com/Building" }] }"#,
516 ValueHint::new(ValueKind::List(&reference_items)),
517 ),
518 (
519 r#"{ "@list": [{ "@value": "1", "@type": "http://www.w3.org/2001/XMLSchema#integer" }, { "@value": "2", "@type": "http://www.w3.org/2001/XMLSchema#integer" }] }"#,
520 ValueHint::new(ValueKind::List(&integer_items)),
521 ),
522 (
523 r#"{ "@list": [{ "@value": { "a": 1 }, "@type": "@json" }] }"#,
524 ValueHint::new(ValueKind::List(&json_items)),
525 ),
526 (r#"{ "@list": [] }"#, ValueHint::new(ValueKind::List(&[]))),
527 (
528 r#"{ "@list": [{ "@value": "a" }], "@index": "i" }"#,
529 ValueHint::indexed(ValueKind::List(&plain_items[..1])),
530 ),
531 ];
532
533 let mut selected = BTreeSet::new();
534 for context in &contexts {
535 for (source, hint) in &cases {
536 selected.extend(assert_same_selection(context, &expanded_object(source), *hint));
537 }
538 }
539
540 assert!(selected.len() >= 14, "too few distinct terms selected: {selected:?}");
543 }
544
545 #[tokio::test]
548 async fn empty_array_hint_selects_the_term_of_an_empty_property() {
549 let context = processed_context(SELECTION_CONTEXT).await;
550 let empty_node = Indexed::new(Object::node(Node::new()), None);
551 let term = assert_same_selection(&context, &empty_node, ValueHint::new(ValueKind::EmptyArray));
552 assert_eq!(term.as_deref(), Some("nameById"));
553 }
554}