1use std::collections::HashMap;
2use std::future::Future;
3use std::ops::Range;
4use std::pin::Pin;
5
6use rowan::SyntaxNode;
7
8use crate::Spanned;
9use crate::model::*;
10
11use super::parser::{Lang, SyntaxKind};
12
13type Node = SyntaxNode<Lang>;
14
15const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
17const RDF_FIRST: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#first";
18const RDF_REST: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#rest";
19const RDF_NIL: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil";
20const RDF_JSON: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON";
21const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
22const XSD_INTEGER: &str = "http://www.w3.org/2001/XMLSchema#integer";
23const XSD_DOUBLE: &str = "http://www.w3.org/2001/XMLSchema#double";
24const XSD_BOOLEAN: &str = "http://www.w3.org/2001/XMLSchema#boolean";
25
26#[derive(Debug, Clone)]
29pub enum JsonLdVal {
30 Object(
36 Vec<(String, Range<usize>, Range<usize>, JsonLdVal)>,
37 Range<usize>,
38 ),
39 Array(Vec<(JsonLdVal, Range<usize>)>),
40 Str(String),
41 Number(String),
42 Bool(bool),
43 Null,
44 Invalid,
45}
46
47impl JsonLdVal {
48 pub fn get(&self, key: &str) -> Option<&JsonLdVal> {
50 if let JsonLdVal::Object(members, _) = self {
51 members
52 .iter()
53 .find(|(k, _, _, _)| k == key)
54 .map(|(_, _, _, v)| v)
55 } else {
56 None
57 }
58 }
59
60 pub fn as_str(&self) -> Option<&str> {
62 if let JsonLdVal::Str(s) = self {
63 Some(s.as_str())
64 } else {
65 None
66 }
67 }
68
69 pub fn as_bool(&self) -> Option<bool> {
71 if let JsonLdVal::Bool(b) = self {
72 Some(*b)
73 } else {
74 None
75 }
76 }
77
78 pub fn as_object(&self) -> Option<&[(String, Range<usize>, Range<usize>, JsonLdVal)]> {
81 if let JsonLdVal::Object(members, _) = self {
82 Some(members.as_slice())
83 } else {
84 None
85 }
86 }
87
88 pub fn as_array(&self) -> Option<&[(JsonLdVal, Range<usize>)]> {
91 if let JsonLdVal::Array(items) = self {
92 Some(items.as_slice())
93 } else {
94 None
95 }
96 }
97}
98
99#[derive(Debug, Clone, Default)]
102pub struct TermDefinition {
103 pub iri: Option<String>,
104 pub type_mapping: Option<String>,
105 pub language: Option<String>,
106 pub container: Option<String>,
107 pub reverse: bool,
108}
109
110#[derive(Debug, Clone, Default)]
114pub struct ActiveContext {
115 pub base: Option<String>,
116 pub vocab: Option<String>,
117 pub language: Option<String>,
118 pub terms: HashMap<String, TermDefinition>,
119}
120
121pub trait ContextLoader {
139 fn load<'a>(&'a mut self, url: &'a str) -> Pin<Box<dyn Future<Output = Option<String>> + 'a>>;
140
141 fn load_val<'a>(
145 &'a mut self,
146 url: &'a str,
147 ) -> Pin<Box<dyn Future<Output = Option<JsonLdVal>> + 'a>> {
148 Box::pin(async move {
149 let content = self.load(url).await?;
150 parse_jsonld_for_context(&content)
151 })
152 }
153}
154
155pub struct NoopContextLoader;
157
158impl ContextLoader for NoopContextLoader {
159 fn load<'a>(&'a mut self, _url: &'a str) -> Pin<Box<dyn Future<Output = Option<String>> + 'a>> {
160 Box::pin(std::future::ready(None))
161 }
162}
163
164struct ConvertState {
167 blank_counter: usize,
168 triples: Vec<Spanned<Triple>>,
169 prefixes: Vec<Spanned<TurtlePrefix>>,
170 accumulated_context: ActiveContext,
173 initial_base: Option<String>,
176}
177
178impl ConvertState {
179 fn new(initial_base: Option<String>) -> Self {
180 Self {
181 blank_counter: 0,
182 triples: Vec::new(),
183 prefixes: Vec::new(),
184 accumulated_context: ActiveContext::default(),
185 initial_base,
186 }
187 }
188
189 fn register_prefix(
190 &mut self,
191 term: &str,
192 iri: &str,
193 term_span: Range<usize>,
194 val_span: Range<usize>,
195 ) {
196 self.prefixes.retain(|p| p.value().prefix.value() != term);
199 let full_span = term_span.start..val_span.end;
200 self.prefixes.push(Spanned(
201 TurtlePrefix {
202 span: full_span.clone(),
203 prefix: Spanned(term.to_string(), term_span),
204 value: Spanned(NamedNode::Full(iri.to_string(), val_span.start), val_span),
205 },
206 full_span,
207 ));
208 }
209
210 fn fresh_blank(&mut self, offset: usize) -> Term {
211 let id = self.blank_counter;
212 self.blank_counter += 1;
213 Term::BlankNode(BlankNode::Named(format!("b{}", id), offset))
214 }
215
216 fn add_triple(
217 &mut self,
218 subject: (Term, Range<usize>),
219 predicate: (Term, Range<usize>),
220 object: (Term, Range<usize>),
221 graph: Option<(Term, Range<usize>)>,
222 span: Range<usize>,
223 ) {
224 let s = span.clone();
225 self.triples.push(Spanned(
226 Triple {
227 subject: Spanned(subject.0, subject.1),
228 po: vec![Spanned(
229 PO {
230 predicate: Spanned(predicate.0, predicate.1),
231 object: vec![Spanned(object.0, object.1)],
232 },
233 s.clone(),
234 )],
235 graph: graph.map(|g| Spanned(g.0, g.1)),
236 },
237 s,
238 ));
239 }
240
241 fn named(iri: &str) -> Term {
242 Term::NamedNode(NamedNode::Full(iri.to_string(), 0))
243 }
244
245 fn into_turtle(self) -> (Turtle, ActiveContext) {
246 let turtle_base = self.accumulated_context.base.as_deref().map(|iri| {
251 let nn = NamedNode::Full(iri.to_string(), 0);
252 Spanned(Base(0..0, Spanned(nn, 0..0)), 0..0)
253 });
254 let mut turtle = Turtle::new(turtle_base, self.prefixes, self.triples);
255 turtle.set_base = self.initial_base;
256 (turtle, self.accumulated_context)
257 }
258}
259
260fn text_range(node: &Node) -> Range<usize> {
263 let r = node.text_range();
264 r.start().into()..r.end().into()
265}
266
267fn child(node: &Node, kind: SyntaxKind) -> Option<Node> {
268 node.children().find(|c| c.kind() == kind)
269}
270
271fn children(node: &Node, kind: SyntaxKind) -> impl Iterator<Item = Node> {
272 node.children().filter(move |c| c.kind() == kind)
273}
274
275fn extract_json_string(node: &Node) -> Option<String> {
282 let terminal_wrapper = node.children().next()?;
284 let tok = terminal_wrapper
285 .children_with_tokens()
286 .filter_map(|e| e.into_token())
287 .find(|t| !matches!(t.kind(), SyntaxKind::WhiteSpace | SyntaxKind::Comment))?;
288 let text = tok.text();
289 if text.starts_with('"') && text.ends_with('"') && text.len() >= 2 {
290 Some(unescape_json_string(&text[1..text.len() - 1]))
291 } else {
292 None
293 }
294}
295
296fn terminal_text(wrapper: &Node) -> Option<String> {
298 wrapper
299 .children_with_tokens()
300 .filter_map(|e| e.into_token())
301 .find(|t| !matches!(t.kind(), SyntaxKind::WhiteSpace | SyntaxKind::Comment))
302 .map(|t| t.text().to_string())
303}
304
305fn unescape_json_string(s: &str) -> String {
306 let mut result = String::with_capacity(s.len());
307 let mut chars = s.chars();
308 while let Some(c) = chars.next() {
309 if c == '\\' {
310 match chars.next() {
311 Some('"') => result.push('"'),
312 Some('\\') => result.push('\\'),
313 Some('/') => result.push('/'),
314 Some('b') => result.push('\x08'),
315 Some('f') => result.push('\x0C'),
316 Some('n') => result.push('\n'),
317 Some('r') => result.push('\r'),
318 Some('t') => result.push('\t'),
319 Some('u') => {
320 let hex: String = chars.by_ref().take(4).collect();
321 if let Ok(n) = u32::from_str_radix(&hex, 16) {
322 if let Some(c) = char::from_u32(n) {
323 result.push(c);
324 }
325 }
326 }
327 Some(other) => {
328 result.push('\\');
329 result.push(other);
330 }
331 None => result.push('\\'),
332 }
333 } else {
334 result.push(c);
335 }
336 }
337 result
338}
339
340fn cst_to_value(node: &Node) -> Option<JsonLdVal> {
343 let span = text_range(node);
344 match node.kind() {
345 SyntaxKind::JsonValue => {
346 if let Some(obj) = child(node, SyntaxKind::JsonObject) {
348 return cst_to_value(&obj);
349 }
350 if let Some(arr) = child(node, SyntaxKind::JsonArray) {
351 return cst_to_value(&arr);
352 }
353 if let Some(js) = child(node, SyntaxKind::JsonString) {
354 return cst_to_value(&js);
355 }
356 if let Some(num_node) = child(node, SyntaxKind::JsonNumber) {
360 if let Some(text) = terminal_text(&num_node) {
361 return Some(JsonLdVal::Number(text));
362 }
363 }
364 if child(node, SyntaxKind::TrueLit).is_some() {
365 return Some(JsonLdVal::Bool(true));
366 }
367 if child(node, SyntaxKind::FalseLit).is_some() {
368 return Some(JsonLdVal::Bool(false));
369 }
370 if child(node, SyntaxKind::NullLit).is_some() {
371 return Some(JsonLdVal::Null);
372 }
373 None
374 }
375 SyntaxKind::JsonObject => {
376 let mut members = Vec::new();
377 let member_nodes: Vec<Node> = if let Some(ml) = child(node, SyntaxKind::MemberList) {
380 children(&ml, SyntaxKind::Member).collect()
381 } else {
382 Vec::new()
383 };
384 for member in member_nodes {
385 let Some(key_node) = child(&member, SyntaxKind::JsonString) else {
386 continue;
387 };
388 let Some(key) = extract_json_string(&key_node) else {
389 continue;
390 };
391 let key_span = text_range(&key_node);
392 let Some(val_node) = child(&member, SyntaxKind::JsonValue) else {
393 continue;
394 };
395 let val_span = text_range(&val_node);
396 let val = cst_to_value(&val_node).unwrap_or(JsonLdVal::Invalid);
397 members.push((key, key_span, val_span, val));
398 }
399 Some(JsonLdVal::Object(members, span))
400 }
401 SyntaxKind::JsonArray => {
402 let items = if let Some(vl) = child(node, SyntaxKind::ValueList) {
403 children(&vl, SyntaxKind::JsonValue)
404 .filter_map(|v| {
405 let span = text_range(&v);
406 cst_to_value(&v).map(|val| (val, span))
407 })
408 .collect()
409 } else {
410 Vec::new()
411 };
412 Some(JsonLdVal::Array(items))
413 }
414 SyntaxKind::JsonString => {
415 let s = extract_json_string(node)?;
416 Some(JsonLdVal::Str(s))
417 }
418 _ => None,
419 }
420}
421
422fn process_context<'a>(
429 mut active: ActiveContext,
430 ctx_val: JsonLdVal,
431 loader: &'a mut dyn ContextLoader,
432 state: &'a mut ConvertState,
433) -> Pin<Box<dyn Future<Output = ActiveContext> + 'a>> {
434 Box::pin(async move {
435 match ctx_val {
436 JsonLdVal::Null => {
437 active = ActiveContext::default();
438 }
439 JsonLdVal::Str(ref url) => {
440 let base = active.base.clone().or_else(|| state.initial_base.clone());
444 let resolved_url = resolve_iri(&base, url);
445 if let Some(remote_ctx) = loader.load_val(&resolved_url).await {
446 active = process_context(active, remote_ctx, loader, state).await;
447 }
448 }
449 JsonLdVal::Array(items) => {
450 for (item, _) in items {
451 active = process_context(active, item, loader, state).await;
452 }
453 }
454 JsonLdVal::Object(members, _) => {
455 for (key, key_span, val_span, val) in members {
456 match key.as_str() {
457 "@base" => match &val {
458 JsonLdVal::Str(s) if s.is_empty() => active.base = None,
459 JsonLdVal::Str(s) => {
460 let resolved = resolve_iri(&active.base, s);
461 active.base = Some(resolved.clone());
462 state.accumulated_context.base = Some(resolved);
463 }
464 JsonLdVal::Null => active.base = None,
465 _ => {}
466 },
467 "@vocab" => match val {
468 JsonLdVal::Str(s) => {
469 state.accumulated_context.vocab = Some(s.clone());
470 active.vocab = Some(s);
471 }
472 JsonLdVal::Null => active.vocab = None,
473 _ => {}
474 },
475 "@language" => match val {
476 JsonLdVal::Str(s) => {
477 state.accumulated_context.language = Some(s.clone());
478 active.language = Some(s);
479 }
480 JsonLdVal::Null => active.language = None,
481 _ => {}
482 },
483 k if !k.starts_with('@') => {
485 let scoped_ctx = if let JsonLdVal::Object(ref members, _) = val {
487 members
488 .iter()
489 .find(|(k, _, _, _)| k == "@context")
490 .map(|(_, _, _, v)| v.clone())
491 } else {
492 None
493 };
494
495 if let Some(def) = process_term_definition(&active, k, &val) {
496 if let (Some(iri), false) = (&def.iri, def.reverse) {
497 state.register_prefix(
498 k,
499 iri,
500 key_span.clone(),
501 val_span.clone(),
502 );
503 }
504 state
505 .accumulated_context
506 .terms
507 .insert(k.to_string(), def.clone());
508 active.terms.insert(k.to_string(), def);
509 } else {
510 active.terms.remove(k);
511 }
512
513 if let Some(scoped) = scoped_ctx {
517 active = process_context(active, scoped, loader, state).await;
518 }
519 }
520 _ => {}
521 }
522 }
523 }
524 _ => {}
525 }
526 active
527 })
528}
529
530fn process_term_definition(
531 active: &ActiveContext,
532 term: &str,
533 val: &JsonLdVal,
534) -> Option<TermDefinition> {
535 match val {
536 JsonLdVal::Null => None,
537 JsonLdVal::Str(s) => {
538 let iri = expand_iri(active, s, true, false);
539 Some(TermDefinition {
540 iri,
541 ..Default::default()
542 })
543 }
544 JsonLdVal::Object(members, _) => {
545 let mut def = TermDefinition::default();
546 let map: HashMap<&str, &JsonLdVal> =
547 members.iter().map(|(k, _, _, v)| (k.as_str(), v)).collect();
548
549 if let Some(rev_val) = map.get("@reverse") {
551 if let JsonLdVal::Str(s) = rev_val {
552 def.iri = expand_iri(active, s, true, false);
553 def.reverse = true;
554 }
555 } else if let Some(id_val) = map.get("@id") {
556 match id_val {
557 JsonLdVal::Str(s) => def.iri = expand_iri(active, s, true, false),
558 JsonLdVal::Null => def.iri = None,
559 _ => {}
560 }
561 } else {
562 def.iri = expand_iri(active, term, true, false);
563 }
564
565 if let Some(JsonLdVal::Str(s)) = map.get("@type") {
566 def.type_mapping = Some(s.clone());
567 }
568 if let Some(lang_val) = map.get("@language") {
569 match lang_val {
570 JsonLdVal::Str(s) => def.language = Some(s.clone()),
571 JsonLdVal::Null => def.language = None,
572 _ => {}
573 }
574 }
575 if let Some(JsonLdVal::Str(s)) = map.get("@container") {
576 def.container = Some(s.clone());
577 }
578
579 Some(def)
580 }
581 _ => Some(TermDefinition {
582 iri: None,
583 ..Default::default()
584 }),
585 }
586}
587
588pub fn parse_json(content: &str) -> Option<JsonLdVal> {
594 use super::parser::{Rule, SyntaxKind as SK};
595 let rule = Rule::new(SK::JsonldDoc);
596 let (parse, _) = crate::parse(rule, content);
597 let root = parse.syntax::<Lang>();
598 let json_val_node = root.children().find(|c| c.kind() == SK::JsonValue)?;
599 cst_to_value(&json_val_node)
600}
601
602pub fn parse_jsonld_for_context(content: &str) -> Option<JsonLdVal> {
606 use super::parser::{Rule, SyntaxKind as SK};
607 let rule = Rule::new(SK::JsonldDoc);
608 let (parse, _) = crate::parse(rule, content);
609 let root = parse.syntax::<Lang>();
610 let json_val_node = root.children().find(|c| c.kind() == SK::JsonValue)?;
612 let val = cst_to_value(&json_val_node)?;
613 if let JsonLdVal::Object(members, _) = &val {
615 if let Some((_, _, _, ctx)) = members.iter().find(|(k, _, _, _)| k == "@context") {
616 return Some(ctx.clone());
617 }
618 }
619 Some(val)
620}
621
622fn expand_iri(
625 active: &ActiveContext,
626 value: &str,
627 vocab: bool,
628 document_relative: bool,
629) -> Option<String> {
630 if value.is_empty() {
631 if document_relative {
632 return active.base.clone();
633 }
634 return None;
635 }
636
637 if value.starts_with('@') {
639 return Some(value.to_string());
640 }
641
642 if vocab {
644 if let Some(def) = active.terms.get(value) {
645 if let Some(iri) = &def.iri {
646 if iri == value {
650 return Some(iri.clone());
651 }
652 return expand_iri(active, iri, false, false);
653 }
654 }
655 }
656
657 if let Some(pos) = value.find(':') {
659 if pos > 0 {
660 let prefix = &value[..pos];
661 let suffix = &value[pos + 1..];
662
663 if suffix.starts_with("//") || prefix == "_" {
665 return Some(value.to_string());
666 }
667
668 if let Some(def) = active.terms.get(prefix) {
670 if let Some(iri) = &def.iri {
671 return expand_iri(
672 active,
673 &format!("{}{}", iri, suffix),
674 vocab,
675 document_relative,
676 );
677 }
678 }
679
680 return Some(value.to_string());
682 }
683 }
684
685 if vocab {
687 if let Some(vocab_iri) = &active.vocab {
688 return Some(format!("{}{}", vocab_iri, value));
689 }
690 }
691
692 if document_relative {
694 if let Some(base) = &active.base {
695 return Some(resolve_iri(&Some(base.clone()), value));
696 }
697 }
698
699 Some(value.to_string())
700}
701
702fn resolve_iri(base: &Option<String>, reference: &str) -> String {
703 if let Some(base_str) = base {
705 if let Ok(base_iri) = oxiri::Iri::parse(base_str.as_str()) {
706 if let Ok(resolved) = base_iri.resolve(reference) {
707 return resolved.to_string();
708 }
709 }
710 }
711 reference.to_string()
713}
714
715fn process_document<'a>(
721 state: &'a mut ConvertState,
722 active: &'a ActiveContext,
723 loader: &'a mut dyn ContextLoader,
724 val: &'a JsonLdVal,
725) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
726 Box::pin(async move {
727 match val {
728 JsonLdVal::Object(members, span) => {
729 process_node(state, active, loader, members, span, None).await;
730 }
731 JsonLdVal::Array(items) => {
732 for (item, _) in items {
733 process_document(state, active, loader, item).await;
734 }
735 }
736 _ => {}
737 }
738 })
739}
740
741fn process_document_with_graph<'a>(
746 state: &'a mut ConvertState,
747 active: &'a ActiveContext,
748 loader: &'a mut dyn ContextLoader,
749 val: &'a JsonLdVal,
750 graph: Option<(&'a Term, &'a Range<usize>)>,
751) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
752 Box::pin(async move {
753 match val {
754 JsonLdVal::Object(members, span) => {
755 process_node(state, active, loader, members, span, graph).await;
756 }
757 JsonLdVal::Array(items) => {
758 for (item, _) in items {
759 process_document_with_graph(state, active, loader, item, graph).await;
760 }
761 }
762 _ => {}
763 }
764 })
765}
766
767type MemberMap<'a> = HashMap<&'a str, (&'a Range<usize>, &'a JsonLdVal, &'a Range<usize>)>;
768fn process_node<'a>(
774 state: &'a mut ConvertState,
775 active: &'a ActiveContext,
776 loader: &'a mut dyn ContextLoader,
777 members: &'a [(String, Range<usize>, Range<usize>, JsonLdVal)],
778 span: &'a Range<usize>,
779 graph: Option<(&'a Term, &'a Range<usize>)>,
780) -> Pin<Box<dyn Future<Output = Option<Term>> + 'a>> {
781 Box::pin(async move {
782 let map: MemberMap<'_> = members
783 .iter()
784 .map(|(k, k_span, v_span, v)| (k.as_str(), (k_span, v, v_span)))
785 .collect();
786
787 if map.contains_key("@value") {
789 return process_value_object(active, &map, span);
790 }
791
792 if let Some(list_val) = map.get("@list") {
794 let owned;
795 let items: &[(JsonLdVal, Range<usize>)] = match list_val.1 {
796 JsonLdVal::Array(items) => items.as_slice(),
797 other => {
798 owned = [((*other).clone(), span.clone())];
799 &owned
800 }
801 };
802 return Some(process_list(state, active, loader, items, span, graph).await);
803 }
804
805 if let Some(set_val) = map.get("@set") {
807 if let JsonLdVal::Array(items) = set_val.1 {
808 for (item, _) in items {
809 process_document_with_graph(state, active, loader, item, graph).await;
810 }
811 }
812 return None;
813 }
814
815 let updated;
817 let active: &ActiveContext = if let Some(ctx_val) = map.get("@context") {
818 updated = process_context(active.clone(), ctx_val.1.clone(), loader, state).await;
819 &updated
820 } else {
821 active
822 };
823
824 let id_val_span: Range<usize> = members
826 .iter()
827 .find(|(k, _, _, _)| k == "@id")
828 .map(|(_, _, vs, _)| vs.clone())
829 .unwrap_or_else(|| span.start..span.start);
830 let subject: Term = if let Some(id_val) = map.get("@id") {
831 match id_val.1 {
832 JsonLdVal::Str(s) => match expand_iri(active, s, false, true) {
833 Some(iri) if iri.starts_with("_:") => {
834 Term::BlankNode(BlankNode::Named(iri[2..].to_string(), id_val_span.start))
835 }
836 Some(iri) => {
837 Term::NamedNode(named_from_source(active, s, iri, id_val_span.start, false))
838 }
839 None => state.fresh_blank(id_val_span.start),
840 },
841 _ => state.fresh_blank(id_val_span.start),
842 }
843 } else {
844 state.fresh_blank(span.start)
845 };
846
847 if let Some(type_val) = map.get("@type") {
849 for (type_str, type_span) in collect_strings_spanned(type_val.1, type_val.2) {
850 if let Some(iri) = expand_iri(active, &type_str, true, true) {
851 let obj =
852 term_or_blank_from_source(active, &type_str, iri, type_span.start, true);
853 state.add_triple(
854 (subject.clone(), id_val_span.clone()),
855 (ConvertState::named(RDF_TYPE), type_val.0.clone()), (obj, type_span),
858 graph.map(|(a, b)| (a.clone(), b.clone())),
859 span.clone(),
860 );
861 }
862 }
863 }
864
865 if let Some(graph_val) = map.get("@graph") {
867 let named_graph = subject.clone();
868 match graph_val.1 {
869 JsonLdVal::Array(items) => {
870 for (item, _) in items {
871 process_document_with_graph(
872 state,
873 active,
874 loader,
875 item,
876 Some((&named_graph, graph_val.2)),
877 )
878 .await;
879 }
880 }
881 JsonLdVal::Object(m, s) => {
882 process_node(
883 state,
884 active,
885 loader,
886 m,
887 s,
888 Some((&named_graph, graph_val.2)),
889 )
890 .await;
891 }
892 _ => {}
893 }
894 }
895
896 if let Some(rev_val) = map.get("@reverse") {
898 if let JsonLdVal::Object(rev_members, _) = rev_val.1 {
899 for (prop, prop_span, val_span, val) in rev_members {
900 if prop.starts_with('@') {
901 continue;
902 }
903 if let Some(pred_iri) = expand_iri(active, prop, true, false) {
904 let pred = Term::NamedNode(named_from_source(
905 active,
906 prop,
907 pred_iri,
908 prop_span.start,
909 true,
910 ));
911 let objects =
912 collect_objects(state, active, loader, val, val_span, graph, None)
913 .await;
914 for (obj, obj_span) in objects {
915 state.add_triple(
917 (obj, obj_span.clone()),
918 (pred.clone(), prop_span.clone()),
919 (subject.clone(), id_val_span.clone()),
920 graph.map(|(a, b)| (a.clone(), b.clone())),
921 span.clone(),
922 );
923 }
924 }
925 }
926 }
927 }
928
929 if let Some(included_val) = map.get("@included") {
931 match included_val.1 {
932 JsonLdVal::Array(items) => {
933 for (item, _) in items {
934 process_document(state, active, loader, item).await;
935 }
936 }
937 JsonLdVal::Object(m, s) => {
938 process_node(state, active, loader, m, s, None).await;
939 }
940 _ => {}
941 }
942 }
943
944 for (key, key_span, val_span, val) in members {
946 match key.as_str() {
947 "@context" | "@id" | "@type" | "@graph" | "@reverse" | "@included" | "@nest" => {
948 continue;
949 }
950 k if k.starts_with('@') => continue,
951 _ => {}
952 }
953
954 let pred_iri = match expand_iri(active, key, true, false) {
958 Some(iri) if !iri.starts_with('@') => iri,
959 _ => continue,
960 };
961
962 let term_def = active.terms.get(key.as_str());
963 let objects =
964 collect_objects(state, active, loader, val, val_span, graph, term_def).await;
965 if objects.is_empty() {
966 continue;
967 }
968 let s = span.clone();
969 state.triples.push(Spanned(
970 Triple {
971 subject: Spanned(subject.clone(), id_val_span.clone()),
972 po: vec![Spanned(
973 PO {
974 predicate: Spanned(
975 Term::NamedNode(named_from_source(
976 active,
977 key,
978 pred_iri,
979 key_span.start,
980 true,
981 )),
982 key_span.clone(),
983 ),
984 object: objects
985 .into_iter()
986 .map(|(o, o_span)| Spanned(o, o_span))
987 .collect(),
988 },
989 key_span.clone(),
990 )],
991 graph: graph.map(|g| Spanned(g.0.clone(), g.1.clone())),
992 },
993 s,
994 ));
995 }
996
997 Some(subject)
998 })
999}
1000
1001fn collect_strings(val: &JsonLdVal) -> Vec<String> {
1004 match val {
1005 JsonLdVal::Str(s) => vec![s.clone()],
1006 JsonLdVal::Array(items) => items
1007 .iter()
1008 .filter_map(|(i, _)| {
1009 if let JsonLdVal::Str(s) = i {
1010 Some(s.clone())
1011 } else {
1012 None
1013 }
1014 })
1015 .collect(),
1016 _ => vec![],
1017 }
1018}
1019
1020fn collect_strings_spanned(
1027 val: &JsonLdVal,
1028 fallback: &Range<usize>,
1029) -> Vec<(String, Range<usize>)> {
1030 match val {
1031 JsonLdVal::Str(s) => vec![(s.clone(), fallback.clone())],
1032 JsonLdVal::Array(items) => items
1033 .iter()
1034 .filter_map(|(i, span)| {
1035 if let JsonLdVal::Str(s) = i {
1036 Some((s.clone(), span.clone()))
1037 } else {
1038 None
1039 }
1040 })
1041 .collect(),
1042 _ => vec![],
1043 }
1044}
1045
1046fn collect_objects<'a>(
1050 state: &'a mut ConvertState,
1051 active: &'a ActiveContext,
1052 loader: &'a mut dyn ContextLoader,
1053 val: &'a JsonLdVal,
1054 span: &'a Range<usize>,
1055 graph: Option<(&'a Term, &'a Range<usize>)>,
1056 term_def: Option<&'a TermDefinition>,
1057) -> Pin<Box<dyn Future<Output = Vec<(Term, Range<usize>)>> + 'a>> {
1058 Box::pin(async move {
1059 match val {
1060 JsonLdVal::Array(items) => {
1061 let mut result = Vec::new();
1062 for (item, item_span) in items {
1063 let mut sub =
1064 collect_objects(state, active, loader, item, item_span, graph, term_def)
1065 .await;
1066 result.append(&mut sub);
1067 }
1068 result
1069 }
1070 _ => value_to_rdf(state, active, loader, val, span, graph, term_def)
1071 .await
1072 .into_iter()
1073 .map(|t| (t, span.clone()))
1074 .collect(),
1075 }
1076 })
1077}
1078
1079async fn value_to_rdf<'a>(
1085 state: &'a mut ConvertState,
1086 active: &'a ActiveContext,
1087 loader: &'a mut dyn ContextLoader,
1088 val: &'a JsonLdVal,
1089 span: &'a Range<usize>,
1090 graph: Option<(&'a Term, &'a Range<usize>)>,
1091 term_def: Option<&'a TermDefinition>,
1092) -> Option<Term> {
1093 match val {
1094 JsonLdVal::Object(members, obj_span) => {
1095 let map: MemberMap<'_> = members
1096 .iter()
1097 .map(|(k, k_span, v_span, v)| (k.as_str(), (k_span, v, v_span)))
1098 .collect();
1099 if map.contains_key("@value") {
1100 return process_value_object(active, &map, obj_span);
1101 }
1102 if let Some(list_val) = map.get("@list") {
1103 let owned;
1104 let items: &[(JsonLdVal, Range<usize>)] = match list_val.1 {
1105 JsonLdVal::Array(items) => items.as_slice(),
1106 other => {
1107 owned = [((*other).clone(), obj_span.clone())];
1108 &owned
1109 }
1110 };
1111 return Some(process_list(state, active, loader, items, obj_span, graph).await);
1112 }
1113 process_node(state, active, loader, members, obj_span, graph).await
1114 }
1115
1116 JsonLdVal::Str(s) => {
1117 if let Some(def) = term_def {
1118 match def.type_mapping.as_deref() {
1119 Some("@id") => {
1120 return expand_iri(active, s, false, true).map(|iri| {
1121 term_or_blank_from_source(active, s, iri, span.start, false)
1122 });
1123 }
1124 Some("@vocab") => {
1125 return expand_iri(active, s, true, false).map(|iri| {
1126 Term::NamedNode(named_from_source(active, s, iri, span.start, true))
1127 });
1128 }
1129 Some(ty) if !ty.starts_with('@') => {
1130 let ty_owned = ty.to_string();
1131 if let Some(ty_iri) = expand_iri(active, &ty_owned, true, false) {
1132 return Some(Term::Literal(Literal::RDF(RDFLiteral {
1133 value: s.clone(),
1134 quote_style: StringStyle::Double,
1135 lang: None,
1136 ty: Some(Spanned(
1137 named_from_source(active, ty, ty_iri, 0, true),
1138 0..0,
1139 )),
1140 idx: span.start,
1141 len: span.len(),
1142 })));
1143 }
1144 }
1145 _ => {}
1146 }
1147 if let Some(lang) = &def.language {
1148 return Some(Term::Literal(Literal::RDF(RDFLiteral {
1149 value: s.clone(),
1150 quote_style: StringStyle::Double,
1151 lang: Some(Spanned(lang.clone(), 0..0)),
1152 ty: None,
1153 idx: span.start,
1154 len: span.len(),
1155 })));
1156 }
1157 }
1158 let lang = active.language.clone().map(|l| Spanned(l, 0..0));
1160 let ty = if lang.is_none() {
1161 Some(Spanned(NamedNode::Full(XSD_STRING.to_string(), 0), 0..0))
1162 } else {
1163 None
1164 };
1165 Some(Term::Literal(Literal::RDF(RDFLiteral {
1166 value: s.clone(),
1167 quote_style: StringStyle::Double,
1168 lang,
1169 ty,
1170 idx: span.start,
1171 len: span.len(),
1172 })))
1173 }
1174
1175 JsonLdVal::Number(n) => {
1176 let type_iri = if n.contains('.') || n.to_lowercase().contains('e') {
1177 XSD_DOUBLE
1178 } else {
1179 XSD_INTEGER
1180 };
1181 Some(Term::Literal(Literal::RDF(RDFLiteral {
1182 value: n.clone(),
1183 quote_style: StringStyle::Double,
1184 lang: None,
1185 ty: Some(Spanned(NamedNode::Full(type_iri.to_string(), 0), 0..0)),
1186 idx: span.start,
1187 len: span.len(),
1188 })))
1189 }
1190
1191 JsonLdVal::Bool(b) => Some(Term::Literal(Literal::RDF(RDFLiteral {
1192 value: if *b { "true" } else { "false" }.to_string(),
1193 quote_style: StringStyle::Double,
1194 lang: None,
1195 ty: Some(Spanned(NamedNode::Full(XSD_BOOLEAN.to_string(), 0), 0..0)),
1196 idx: span.start,
1197 len: span.len(),
1198 }))),
1199
1200 JsonLdVal::Invalid => Some(Term::Invalid),
1201 JsonLdVal::Null => None,
1202
1203 JsonLdVal::Array(_) => None,
1205 }
1206}
1207
1208fn process_value_object(
1209 active: &ActiveContext,
1210 map: &MemberMap<'_>,
1211 span: &Range<usize>,
1212) -> Option<Term> {
1213 let value = map.get("@value")?;
1214 let lang_val = map.get("@language");
1215 let type_val = map.get("@type");
1216
1217 let (value_str, is_complex) = match value.1 {
1218 JsonLdVal::Str(s) => (s.clone(), false),
1219 JsonLdVal::Bool(b) => (if *b { "true" } else { "false" }.to_string(), false),
1220 JsonLdVal::Number(n) => (n.clone(), false),
1221 other => (json_serialize(other), true),
1222 };
1223
1224 let type_str = type_val.and_then(|t| {
1225 if let JsonLdVal::Str(s) = t.1 {
1226 Some(s.as_str())
1227 } else {
1228 None
1229 }
1230 });
1231
1232 if type_str == Some("@json") || is_complex {
1234 return Some(Term::Literal(Literal::RDF(RDFLiteral {
1235 value: value_str,
1236 quote_style: StringStyle::Double,
1237 lang: None,
1238 ty: Some(Spanned(NamedNode::Full(RDF_JSON.to_string(), 0), 0..0)),
1239 idx: span.start,
1240 len: span.len(),
1241 })));
1242 }
1243
1244 let lang_tag = lang_val.and_then(|l| {
1246 if let JsonLdVal::Str(s) = l.1 {
1247 Some((s.as_str(), l.2.clone()))
1248 } else {
1249 None
1250 }
1251 });
1252 if let Some((lt, lt_span)) = lang_tag {
1253 return Some(Term::Literal(Literal::RDF(RDFLiteral {
1254 value: value_str,
1255 quote_style: StringStyle::Double,
1256 lang: Some(Spanned(lt.to_string(), lt_span)),
1257 ty: None,
1258 idx: span.start,
1259 len: span.len(),
1260 })));
1261 }
1262
1263 if let Some(ty) = type_str {
1265 let ty_owned = ty.to_string();
1266 let ty_span = type_val.map(|t| t.2.clone()).unwrap_or(0..0);
1267 let ty_node = expand_iri(active, &ty_owned, true, false).map(|iri| {
1268 let start = ty_span.start;
1269 Spanned(named_from_source(active, ty, iri, start, true), ty_span)
1270 });
1271 return Some(Term::Literal(Literal::RDF(RDFLiteral {
1272 value: value_str,
1273 quote_style: StringStyle::Double,
1274 lang: None,
1275 ty: ty_node,
1276 idx: span.start,
1277 len: span.len(),
1278 })));
1279 }
1280
1281 let (inferred_ty, inferred_lang) = match value.1 {
1283 JsonLdVal::Bool(_) => (Some(XSD_BOOLEAN), None),
1284 JsonLdVal::Number(n) => {
1285 if n.contains('.') || n.to_lowercase().contains('e') {
1286 (Some(XSD_DOUBLE), None)
1287 } else {
1288 (Some(XSD_INTEGER), None)
1289 }
1290 }
1291 _ => {
1292 if let Some(lang) = &active.language {
1293 (None, Some(lang.clone()))
1294 } else {
1295 (Some(XSD_STRING), None)
1296 }
1297 }
1298 };
1299
1300 Some(Term::Literal(Literal::RDF(RDFLiteral {
1301 value: value_str,
1302 quote_style: StringStyle::Double,
1303 lang: inferred_lang.map(|l| Spanned(l, 0..0)),
1304 ty: inferred_ty.map(|s| Spanned(NamedNode::Full(s.to_string(), 0), 0..0)),
1305 idx: span.start,
1306 len: span.len(),
1307 })))
1308}
1309
1310fn process_list<'a>(
1315 state: &'a mut ConvertState,
1316 active: &'a ActiveContext,
1317 loader: &'a mut dyn ContextLoader,
1318 items: &'a [(JsonLdVal, Range<usize>)],
1319 span: &'a Range<usize>,
1320 graph: Option<(&'a Term, &'a Range<usize>)>,
1321) -> Pin<Box<dyn Future<Output = Term> + 'a>> {
1322 Box::pin(async move {
1323 if items.is_empty() {
1324 return ConvertState::named(RDF_NIL);
1325 }
1326 let mut rest = ConvertState::named(RDF_NIL);
1328 for (item, item_span) in items.iter().rev() {
1329 let first_term = value_to_rdf(state, active, loader, item, item_span, graph, None)
1330 .await
1331 .unwrap_or(Term::Invalid);
1332 let node = state.fresh_blank(item_span.start);
1333 state.add_triple(
1334 (node.clone(), item_span.start..item_span.start),
1335 (ConvertState::named(RDF_FIRST), 0..0),
1336 (first_term, item_span.clone()),
1337 clone_graph(graph),
1338 item_span.clone(),
1339 );
1340 state.add_triple(
1341 (node.clone(), item_span.start..item_span.start),
1342 (ConvertState::named(RDF_REST), 0..0),
1343 (rest, item_span.start..item_span.start),
1344 clone_graph(graph),
1345 span.clone(),
1346 );
1347 rest = node;
1348 }
1349 rest
1350 })
1351}
1352
1353fn clone_graph(g: Option<(&'_ Term, &'_ Range<usize>)>) -> Option<(Term, Range<usize>)> {
1354 g.map(|g| (g.0.clone(), g.1.clone()))
1355}
1356
1357fn named_from_source(
1373 active: &ActiveContext,
1374 source: &str,
1375 expanded: String,
1376 offset: usize,
1377 vocab: bool,
1378) -> NamedNode {
1379 if vocab && source.contains(':') && active.terms.contains_key(source) {
1383 return NamedNode::Prefixed {
1384 prefix: source.to_string(),
1385 value: String::new(),
1386 idx: offset,
1387 computed: Some(expanded),
1388 };
1389 }
1390
1391 if let Some(pos) = source.find(':') {
1393 if pos > 0 {
1394 let prefix = &source[..pos];
1395 let suffix = &source[pos + 1..];
1396 let known_prefix = active
1397 .terms
1398 .get(prefix)
1399 .and_then(|d| d.iri.as_ref())
1400 .is_some();
1401 if !suffix.starts_with("//") && prefix != "_" && known_prefix {
1402 return NamedNode::Prefixed {
1403 prefix: prefix.to_string(),
1404 value: suffix.to_string(),
1405 idx: offset,
1406 computed: Some(expanded),
1407 };
1408 }
1409 }
1410 }
1411
1412 NamedNode::Full(expanded, offset)
1413}
1414
1415fn term_or_blank_from_source(
1419 active: &ActiveContext,
1420 source: &str,
1421 expanded: String,
1422 offset: usize,
1423 vocab: bool,
1424) -> Term {
1425 if expanded.starts_with("_:") {
1426 Term::BlankNode(BlankNode::Named(expanded[2..].to_string(), offset))
1427 } else {
1428 Term::NamedNode(named_from_source(active, source, expanded, offset, vocab))
1429 }
1430}
1431
1432fn json_serialize(val: &JsonLdVal) -> String {
1433 match val {
1434 JsonLdVal::Object(members, _) => {
1435 let pairs: Vec<String> = members
1436 .iter()
1437 .map(|(k, _, _, v)| format!("\"{}\":{}", json_escape(k), json_serialize(v)))
1438 .collect();
1439 format!("{{{}}}", pairs.join(","))
1440 }
1441 JsonLdVal::Array(items) => {
1442 let elems: Vec<String> = items.iter().map(|(item, _)| json_serialize(item)).collect();
1443 format!("[{}]", elems.join(","))
1444 }
1445 JsonLdVal::Str(s) => format!("\"{}\"", json_escape(s)),
1446 JsonLdVal::Number(n) => n.clone(),
1447 JsonLdVal::Bool(b) => {
1448 if *b {
1449 "true".to_string()
1450 } else {
1451 "false".to_string()
1452 }
1453 }
1454 JsonLdVal::Invalid => "invalid".to_string(),
1455 JsonLdVal::Null => "null".to_string(),
1456 }
1457}
1458
1459fn json_escape(s: &str) -> String {
1460 s.replace('\\', "\\\\").replace('"', "\\\"")
1461}
1462
1463pub fn convert(root: &Node, base_iri: Option<String>) -> (Turtle, ActiveContext) {
1477 use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
1480
1481 fn noop_clone(p: *const ()) -> RawWaker {
1482 RawWaker::new(p, &NOOP_VTABLE)
1483 }
1484 fn noop(_: *const ()) {}
1485 static NOOP_VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop);
1486
1487 let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &NOOP_VTABLE)) };
1492 let mut cx = Context::from_waker(&waker);
1493 let mut loader = NoopContextLoader;
1494 let mut fut = std::pin::pin!(convert_with_loader(root, &mut loader, base_iri));
1495 match fut.as_mut().poll(&mut cx) {
1496 Poll::Ready(v) => v,
1497 Poll::Pending => unreachable!("NoopContextLoader always resolves immediately"),
1498 }
1499}
1500
1501pub async fn convert_with_loader(
1511 root: &Node,
1512 loader: &mut dyn ContextLoader,
1513 base_iri: Option<String>,
1514) -> (Turtle, ActiveContext) {
1515 let mut state = ConvertState::new(base_iri.clone());
1516 let active = ActiveContext {
1517 base: base_iri,
1518 ..Default::default()
1519 };
1520
1521 if let Some(json_val_node) = root.children().find(|c| c.kind() == SyntaxKind::JsonValue) {
1523 if let Some(val) = cst_to_value(&json_val_node) {
1524 process_document(&mut state, &active, loader, &val).await;
1525 }
1526 }
1527
1528 state.into_turtle()
1529}
1530
1531#[cfg(test)]
1534mod tests {
1535 use super::super::parser::{Rule, SyntaxKind as SK};
1536 use super::*;
1537
1538 fn parse_jsonld(input: &str) -> Turtle {
1539 use crate::parse as crate_parse;
1540 let rule = Rule::new(SK::JsonldDoc);
1541 let (result, _) = crate_parse(rule, input);
1542 let root = result.syntax::<Lang>();
1543 convert(&root, None).0
1544 }
1545
1546 fn parse_jsonld_with_context(input: &str) -> (Turtle, ActiveContext) {
1547 use crate::parse as crate_parse;
1548 let rule = Rule::new(SK::JsonldDoc);
1549 let (result, _) = crate_parse(rule, input);
1550 let root = result.syntax::<Lang>();
1551 convert(&root, None)
1552 }
1553
1554 fn parse_jsonld_with_base(input: &str, base_iri: &str) -> (Turtle, ActiveContext) {
1555 use crate::parse as crate_parse;
1556 let rule = Rule::new(SK::JsonldDoc);
1557 let (result, _) = crate_parse(rule, input);
1558 let root = result.syntax::<Lang>();
1559 convert(&root, Some(base_iri.to_string()))
1560 }
1561
1562 fn triples_of(t: &Turtle) -> &[Spanned<Triple>] {
1563 &t.triples
1564 }
1565
1566 fn nn_iri(nn: &NamedNode) -> Option<&str> {
1569 match nn {
1570 NamedNode::Full(s, _) => Some(s.as_str()),
1571 NamedNode::Prefixed { computed, .. } => computed.as_deref(),
1572 _ => None,
1573 }
1574 }
1575
1576 fn term_iri(term: &Term) -> Option<&str> {
1577 match term {
1578 Term::NamedNode(nn) => nn_iri(nn),
1579 _ => None,
1580 }
1581 }
1582
1583 fn subject_iri(triple: &Triple) -> Option<&str> {
1584 term_iri(&triple.subject.0)
1585 }
1586
1587 fn predicate_iri(triple: &Triple) -> Option<&str> {
1588 triple.po.first().and_then(|po| term_iri(&po.predicate.0))
1589 }
1590
1591 fn object_iri(triple: &Triple) -> Option<&str> {
1592 triple
1593 .po
1594 .first()
1595 .and_then(|po| po.object.first())
1596 .and_then(|o| term_iri(&o.0))
1597 }
1598
1599 fn object_literal(triple: &Triple) -> Option<(&str, Option<&str>, Option<&str>)> {
1600 triple
1601 .po
1602 .first()
1603 .and_then(|po| po.object.first())
1604 .and_then(|o| {
1605 if let Term::Literal(Literal::RDF(lit)) = &o.0 {
1606 let lang = lit.lang.as_ref().map(|l| l.as_str());
1607 let ty = lit.ty.as_ref().and_then(|sp| nn_iri(sp.value()));
1608 Some((lit.value.as_str(), lang, ty))
1609 } else {
1610 None
1611 }
1612 })
1613 }
1614
1615 #[test]
1618 fn test_basic_triple() {
1619 let t = parse_jsonld(
1620 r#"
1621 {
1622 "@id": "http://example.org/s",
1623 "http://example.org/p": "hello"
1624 }
1625 "#,
1626 );
1627 let triples = triples_of(&t);
1628 assert_eq!(triples.len(), 1);
1629 assert_eq!(subject_iri(&triples[0]), Some("http://example.org/s"));
1630 assert_eq!(predicate_iri(&triples[0]), Some("http://example.org/p"));
1631 assert_eq!(
1632 object_literal(&triples[0]),
1633 Some(("hello", None, Some(XSD_STRING)))
1634 );
1635 }
1636
1637 #[test]
1640 fn test_multiple_properties() {
1641 let t = parse_jsonld(
1642 r#"
1643 {
1644 "@id": "http://example.org/s",
1645 "http://example.org/p1": "a",
1646 "http://example.org/p2": "b"
1647 }
1648 "#,
1649 );
1650 assert_eq!(triples_of(&t).len(), 2);
1651 }
1652
1653 #[test]
1656 fn test_nested_blank_node() {
1657 let t = parse_jsonld(
1658 r#"
1659 {
1660 "@id": "http://example.org/s",
1661 "http://example.org/p": {
1662 "http://example.org/q": "inner"
1663 }
1664 }
1665 "#,
1666 );
1667 assert_eq!(triples_of(&t).len(), 2);
1669 let outer = &triples_of(&t)[1]; assert_eq!(subject_iri(outer), Some("http://example.org/s"));
1671 if let Some(po) = outer.po.first() {
1673 if let Some(obj) = po.object.first() {
1674 assert!(matches!(obj.0, Term::BlankNode(_)));
1675 } else {
1676 panic!("no object");
1677 }
1678 }
1679 }
1680
1681 #[test]
1684 fn test_inline_context_term() {
1685 let t = parse_jsonld(
1686 r#"
1687 {
1688 "@context": { "name": "http://schema.org/name" },
1689 "@id": "http://example.org/alice",
1690 "name": "Alice"
1691 }
1692 "#,
1693 );
1694 let triples = triples_of(&t);
1695 assert_eq!(triples.len(), 1);
1696 assert_eq!(predicate_iri(&triples[0]), Some("http://schema.org/name"));
1697 assert_eq!(
1698 object_literal(&triples[0]),
1699 Some(("Alice", None, Some(XSD_STRING)))
1700 );
1701 }
1702
1703 #[test]
1706 fn test_vocab_expansion() {
1707 let t = parse_jsonld(
1708 r#"
1709 {
1710 "@context": { "@vocab": "http://schema.org/" },
1711 "@id": "http://example.org/alice",
1712 "name": "Alice"
1713 }
1714 "#,
1715 );
1716 let triples = triples_of(&t);
1717 assert_eq!(triples.len(), 1);
1718 assert_eq!(predicate_iri(&triples[0]), Some("http://schema.org/name"));
1719 }
1720
1721 #[test]
1724 fn test_type_triple() {
1725 let t = parse_jsonld(
1726 r#"
1727 {
1728 "@id": "http://example.org/alice",
1729 "@type": "http://schema.org/Person"
1730 }
1731 "#,
1732 );
1733 let triples = triples_of(&t);
1734 assert_eq!(triples.len(), 1);
1735 assert_eq!(predicate_iri(&triples[0]), Some(RDF_TYPE));
1736 assert_eq!(object_iri(&triples[0]), Some("http://schema.org/Person"));
1737 }
1738
1739 #[test]
1742 fn test_type_array() {
1743 let t = parse_jsonld(
1744 r#"
1745 {
1746 "@id": "http://example.org/x",
1747 "@type": ["http://example.org/A", "http://example.org/B"]
1748 }
1749 "#,
1750 );
1751 assert_eq!(triples_of(&t).len(), 2);
1752 }
1753
1754 #[test]
1757 fn test_value_object_language() {
1758 let t = parse_jsonld(
1759 r#"
1760 {
1761 "@id": "http://example.org/s",
1762 "http://example.org/p": { "@value": "hello", "@language": "en" }
1763 }
1764 "#,
1765 );
1766 let triples = triples_of(&t);
1767 assert_eq!(triples.len(), 1);
1768 assert_eq!(
1769 object_literal(&triples[0]),
1770 Some(("hello", Some("en"), None))
1771 );
1772 }
1773
1774 #[test]
1775 fn test_type_array_per_element_spans() {
1776 let input = r#"{
1777 "@id": "http://example.org/s",
1778 "@type": ["http://example.org/A", "http://example.org/B"]
1779}"#;
1780 let t = parse_jsonld(input);
1781 let mut type_spans: Vec<&str> = Vec::new();
1782 for tr in triples_of(&t) {
1783 for po in &tr.po {
1784 let is_type = matches!(
1785 &po.predicate.0,
1786 Term::NamedNode(NamedNode::Full(s, _)) if s == RDF_TYPE
1787 );
1788 if !is_type {
1789 continue;
1790 }
1791 for o in &po.object {
1792 type_spans.push(&input[o.1.clone()]);
1793 }
1794 }
1795 }
1796 assert_eq!(
1798 type_spans,
1799 vec!["\"http://example.org/A\"", "\"http://example.org/B\"",]
1800 );
1801 }
1802
1803 #[test]
1806 fn test_value_object_typed() {
1807 let t = parse_jsonld(
1808 r#"
1809 {
1810 "@id": "http://example.org/s",
1811 "http://example.org/p": { "@value": "42", "@type": "http://www.w3.org/2001/XMLSchema#integer" }
1812 }
1813 "#,
1814 );
1815 let triples = triples_of(&t);
1816 assert_eq!(triples.len(), 1);
1817 assert_eq!(
1818 object_literal(&triples[0]),
1819 Some(("42", None, Some(XSD_INTEGER)))
1820 );
1821 }
1822
1823 #[test]
1826 fn test_native_integer() {
1827 let t = parse_jsonld(
1828 r#"
1829 { "@id": "http://example.org/s", "http://example.org/p": 42 }
1830 "#,
1831 );
1832 assert_eq!(
1833 object_literal(&triples_of(&t)[0]),
1834 Some(("42", None, Some(XSD_INTEGER)))
1835 );
1836 }
1837
1838 #[test]
1839 fn test_native_double() {
1840 let t = parse_jsonld(
1841 r#"
1842 { "@id": "http://example.org/s", "http://example.org/p": 3.14 }
1843 "#,
1844 );
1845 assert_eq!(
1846 object_literal(&triples_of(&t)[0]),
1847 Some(("3.14", None, Some(XSD_DOUBLE)))
1848 );
1849 }
1850
1851 #[test]
1854 fn test_boolean() {
1855 let t = parse_jsonld(
1856 r#"
1857 { "@id": "http://example.org/s", "http://example.org/p": true }
1858 "#,
1859 );
1860 assert_eq!(
1861 object_literal(&triples_of(&t)[0]),
1862 Some(("true", None, Some(XSD_BOOLEAN)))
1863 );
1864 }
1865
1866 #[test]
1869 fn test_null_skipped() {
1870 let t = parse_jsonld(
1871 r#"
1872 { "@id": "http://example.org/s", "http://example.org/p": null }
1873 "#,
1874 );
1875 assert_eq!(triples_of(&t).len(), 0);
1876 }
1877
1878 #[test]
1881 fn test_list() {
1882 let t = parse_jsonld(
1883 r#"
1884 {
1885 "@id": "http://example.org/s",
1886 "http://example.org/p": { "@list": ["a", "b"] }
1887 }
1888 "#,
1889 );
1890 let triples = triples_of(&t);
1892 assert!(triples.len() >= 3);
1893 let prop_triple = triples.last().unwrap();
1895 if let Some(po) = prop_triple.po.first() {
1896 if let Some(obj) = po.object.first() {
1897 assert!(matches!(obj.0, Term::BlankNode(_)));
1898 }
1899 }
1900 let has_first = triples.iter().any(|t| predicate_iri(t) == Some(RDF_FIRST));
1902 let has_rest = triples.iter().any(|t| predicate_iri(t) == Some(RDF_REST));
1903 assert!(has_first);
1904 assert!(has_rest);
1905 }
1906
1907 #[test]
1910 fn test_empty_list() {
1911 let t = parse_jsonld(
1912 r#"
1913 {
1914 "@id": "http://example.org/s",
1915 "http://example.org/p": { "@list": [] }
1916 }
1917 "#,
1918 );
1919 let triples = triples_of(&t);
1920 assert_eq!(triples.len(), 1);
1921 assert_eq!(object_iri(&triples[0]), Some(RDF_NIL));
1922 }
1923
1924 #[test]
1927 fn test_named_graph() {
1928 let t = parse_jsonld(
1929 r#"
1930 {
1931 "@id": "http://example.org/g",
1932 "@graph": [
1933 { "@id": "http://example.org/s", "http://example.org/p": "v" }
1934 ]
1935 }
1936 "#,
1937 );
1938 let triples = triples_of(&t);
1939 assert_eq!(triples.len(), 1);
1941 let triple = &triples[0];
1942 if let Some(g) = &triple.graph {
1943 if let Term::NamedNode(NamedNode::Full(g_iri, _)) = &g.0 {
1944 assert_eq!(g_iri, "http://example.org/g");
1945 } else {
1946 panic!("graph should be a named node");
1947 }
1948 } else {
1949 panic!("triple should be in a named graph");
1950 }
1951 }
1952
1953 #[test]
1956 fn test_reverse() {
1957 let t = parse_jsonld(
1958 r#"
1959 {
1960 "@id": "http://example.org/child",
1961 "@reverse": {
1962 "http://example.org/parent": { "@id": "http://example.org/dad" }
1963 }
1964 }
1965 "#,
1966 );
1967 let triples = triples_of(&t);
1969 assert_eq!(triples.len(), 1);
1970 assert_eq!(subject_iri(&triples[0]), Some("http://example.org/dad"));
1971 assert_eq!(object_iri(&triples[0]), Some("http://example.org/child"));
1972 }
1973
1974 #[test]
1977 fn test_top_level_array() {
1978 let t = parse_jsonld(
1979 r#"
1980 [
1981 { "@id": "http://example.org/a", "http://example.org/p": "x" },
1982 { "@id": "http://example.org/b", "http://example.org/p": "y" }
1983 ]
1984 "#,
1985 );
1986 assert_eq!(triples_of(&t).len(), 2);
1987 }
1988
1989 #[test]
1992 fn test_compact_iri() {
1993 let t = parse_jsonld(
1994 r#"
1995 {
1996 "@context": {
1997 "schema": "http://schema.org/",
1998 "name": "schema:name"
1999 },
2000 "@id": "http://example.org/alice",
2001 "name": "Alice"
2002 }
2003 "#,
2004 );
2005 let triples = triples_of(&t);
2006 assert_eq!(triples.len(), 1);
2007 assert_eq!(predicate_iri(&triples[0]), Some("http://schema.org/name"));
2008 }
2009
2010 #[test]
2011 fn test_compact_predicate_kept_prefixed_with_computed() {
2012 let input = r#"{"@context":{"foaf":"http://xmlns.com/foaf/0.1/"},"@id":"http://example.org/alice","foaf:name":"Alice"}"#;
2016 let t = parse_jsonld(input);
2017 let triples = triples_of(&t);
2018 assert_eq!(triples.len(), 1);
2019 match &triples[0].po[0].predicate.0 {
2020 Term::NamedNode(NamedNode::Prefixed {
2021 prefix,
2022 value,
2023 computed,
2024 ..
2025 }) => {
2026 assert_eq!(prefix, "foaf");
2027 assert_eq!(value, "name");
2028 assert_eq!(
2029 computed.as_deref(),
2030 Some("http://xmlns.com/foaf/0.1/name"),
2031 "computed should hold the context-resolved absolute IRI"
2032 );
2033 }
2034 other => panic!("expected a prefixed predicate, got {:?}", other),
2035 }
2036 assert_eq!(
2038 predicate_iri(&triples[0]),
2039 Some("http://xmlns.com/foaf/0.1/name")
2040 );
2041 }
2042
2043 #[test]
2044 fn test_whole_term_compact_iri_takes_precedence() {
2045 let input = r#"{"@context":{"foaf":"http://xmlns.com/foaf/0.1/","foaf:name":"http://example.org/fullName"},"@id":"http://example.org/alice","foaf:name":"Alice"}"#;
2050 let t = parse_jsonld(input);
2051 let triples = triples_of(&t);
2052 assert_eq!(triples.len(), 1);
2053 match &triples[0].po[0].predicate.0 {
2054 Term::NamedNode(NamedNode::Prefixed {
2055 prefix,
2056 value,
2057 computed,
2058 ..
2059 }) => {
2060 assert_eq!(prefix, "foaf:name");
2061 assert_eq!(value, "");
2062 assert_eq!(computed.as_deref(), Some("http://example.org/fullName"));
2063 }
2064 other => panic!("expected a prefixed predicate, got {:?}", other),
2065 }
2066 assert_eq!(
2067 predicate_iri(&triples[0]),
2068 Some("http://example.org/fullName")
2069 );
2070 assert_eq!(format!("{}", triples[0].po[0].predicate.0), "foaf:name");
2072 }
2073
2074 #[test]
2077 fn test_type_coercion_id() {
2078 let t = parse_jsonld(
2079 r#"
2080 {
2081 "@context": {
2082 "knows": { "@id": "http://schema.org/knows", "@type": "@id" }
2083 },
2084 "@id": "http://example.org/alice",
2085 "knows": "http://example.org/bob"
2086 }
2087 "#,
2088 );
2089 let triples = triples_of(&t);
2090 assert_eq!(triples.len(), 1);
2091 assert_eq!(object_iri(&triples[0]), Some("http://example.org/bob"));
2092 }
2093
2094 #[test]
2097 fn test_default_language() {
2098 let t = parse_jsonld(
2099 r#"
2100 {
2101 "@context": { "@language": "fr" },
2102 "@id": "http://example.org/s",
2103 "http://example.org/p": "Bonjour"
2104 }
2105 "#,
2106 );
2107 let triples = triples_of(&t);
2108 assert_eq!(triples.len(), 1);
2109 assert_eq!(
2110 object_literal(&triples[0]),
2111 Some(("Bonjour", Some("fr"), None))
2112 );
2113 }
2114
2115 #[test]
2118 fn test_error_recovery() {
2119 let t = parse_jsonld(r#"{ "@id": "http://example.org/s", "http://example.org/p": "v" "#);
2121 let _ = triples_of(&t);
2124 }
2125
2126 fn offset_of(haystack: &str, needle: &str) -> usize {
2133 haystack
2134 .find(needle)
2135 .unwrap_or_else(|| panic!("needle {:?} not found in {:?}", needle, haystack))
2136 }
2137
2138 #[test]
2139 fn test_subject_span_points_to_id_value() {
2140 let input = r#"{"@id":"http://example.org/s","http://example.org/p":"v"}"#;
2142 let t = parse_jsonld(input);
2143 let triples = triples_of(&t);
2144 assert_eq!(triples.len(), 1);
2145
2146 let expected_start = offset_of(input, "\"http://example.org/s\"");
2147 let subject_span = &triples[0].subject.1;
2148 assert_eq!(
2149 subject_span.start, expected_start,
2150 "subject span should start at the @id value, not the whole object"
2151 );
2152 }
2153
2154 #[test]
2155 fn test_predicate_span_points_to_key_field() {
2156 let input = r#"{"@id":"http://example.org/s","http://example.org/p":"v"}"#;
2157 let t = parse_jsonld(input);
2158 let triples = triples_of(&t);
2159 assert_eq!(triples.len(), 1);
2160
2161 let expected_start = offset_of(input, "\"http://example.org/p\"");
2162 let pred_span = &triples[0].po[0].predicate.1;
2163 assert_eq!(
2164 pred_span.start, expected_start,
2165 "predicate span should start at the key field"
2166 );
2167 }
2168
2169 #[test]
2170 fn test_predicate_span_points_to_compact_iri_key_not_expanded_iri() {
2171 let input = r#"{"@context":{"foaf":"http://xmlns.com/foaf/0.1/"},"@id":"http://example.org/alice","foaf:name":"Alice"}"#;
2175 let t = parse_jsonld(input);
2176 let triples = triples_of(&t);
2177 assert_eq!(triples.len(), 1);
2178 assert_eq!(
2179 predicate_iri(&triples[0]),
2180 Some("http://xmlns.com/foaf/0.1/name")
2181 );
2182
2183 let expected_start = offset_of(input, "\"foaf:name\"");
2184 let pred_span = &triples[0].po[0].predicate.1;
2185 assert_eq!(
2186 pred_span.start, expected_start,
2187 "predicate span should point to the compact key in source, not the expanded IRI"
2188 );
2189 assert_eq!(
2191 pred_span.end,
2192 expected_start + "\"foaf:name\"".len(),
2193 "predicate span end should be just past the compact key"
2194 );
2195 }
2196
2197 #[test]
2198 fn test_object_span_points_to_value() {
2199 let input = r#"{"@id":"http://example.org/s","http://example.org/p":"hello"}"#;
2200 let t = parse_jsonld(input);
2201 let triples = triples_of(&t);
2202 assert_eq!(triples.len(), 1);
2203
2204 let expected_start = offset_of(input, "\"hello\"");
2205 let obj_span = &triples[0].po[0].object[0].1;
2206 assert_eq!(
2207 obj_span.start, expected_start,
2208 "object span should start at the value, not the whole object"
2209 );
2210 }
2211
2212 #[test]
2213 fn test_object_span_for_iri_value() {
2214 let input = r#"{"@context":{"p":{"@id":"http://example.org/p","@type":"@id"}},"@id":"http://example.org/s","p":"http://example.org/o"}"#;
2216 let t = parse_jsonld(input);
2217 let triples = triples_of(&t);
2218 assert_eq!(triples.len(), 1);
2219
2220 let expected_start = offset_of(input, "\"http://example.org/o\"");
2222 let obj_span = &triples[0].po[0].object[0].1;
2223 assert_eq!(
2224 obj_span.start, expected_start,
2225 "object IRI span should point to the value string"
2226 );
2227 }
2228
2229 #[test]
2230 fn test_object_span_for_number() {
2231 let input = r#"{"@id":"http://example.org/s","http://example.org/p":42}"#;
2232 let t = parse_jsonld(input);
2233 let triples = triples_of(&t);
2234 assert_eq!(triples.len(), 1);
2235
2236 let expected_start = offset_of(input, "42");
2237 let obj_span = &triples[0].po[0].object[0].1;
2238 assert_eq!(
2239 obj_span.start, expected_start,
2240 "numeric object span should start at the number token"
2241 );
2242 }
2243
2244 #[test]
2245 fn test_spans_are_independent_per_property() {
2246 let input = r#"{"@id":"http://example.org/s","http://example.org/p1":"v1","http://example.org/p2":"v2"}"#;
2248 let t = parse_jsonld(input);
2249 let triples = triples_of(&t);
2250 assert_eq!(triples.len(), 2);
2251
2252 let id_start = offset_of(input, "\"http://example.org/s\"");
2254 for triple in triples {
2255 assert_eq!(triple.subject.1.start, id_start);
2256 }
2257
2258 let p1_start = offset_of(input, "\"http://example.org/p1\"");
2260 let p2_start = offset_of(input, "\"http://example.org/p2\"");
2261 let pred_starts: Vec<usize> = triples.iter().map(|t| t.po[0].predicate.1.start).collect();
2262 assert!(pred_starts.contains(&p1_start), "p1 span missing");
2263 assert!(pred_starts.contains(&p2_start), "p2 span missing");
2264
2265 let v1_start = offset_of(input, "\"v1\"");
2266 let v2_start = offset_of(input, "\"v2\"");
2267 let obj_starts: Vec<usize> = triples.iter().map(|t| t.po[0].object[0].1.start).collect();
2268 assert!(obj_starts.contains(&v1_start), "v1 span missing");
2269 assert!(obj_starts.contains(&v2_start), "v2 span missing");
2270 }
2271
2272 fn block_on_ready<T>(fut: impl std::future::Future<Output = T>) -> T {
2279 use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
2280 fn clone(p: *const ()) -> RawWaker {
2281 RawWaker::new(p, &VTABLE)
2282 }
2283 fn noop(_: *const ()) {}
2284 static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
2285 let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
2286 let mut cx = Context::from_waker(&waker);
2287 let mut pinned = std::pin::pin!(fut);
2288 match pinned.as_mut().poll(&mut cx) {
2289 Poll::Ready(v) => v,
2290 Poll::Pending => panic!("future did not complete synchronously"),
2291 }
2292 }
2293
2294 #[test]
2295 fn test_remote_context_loader() {
2296 struct MockLoader;
2298 impl ContextLoader for MockLoader {
2299 fn load<'a>(
2300 &'a mut self,
2301 url: &'a str,
2302 ) -> Pin<Box<dyn Future<Output = Option<String>> + 'a>> {
2303 let result = if url == "https://example.org/context.jsonld" {
2304 Some(
2305 r#"{
2306 "@context": {
2307 "name": "http://schema.org/name",
2308 "email": "http://schema.org/email",
2309 "Person": "http://schema.org/Person"
2310 }
2311 }"#
2312 .to_string(),
2313 )
2314 } else {
2315 None
2316 };
2317 Box::pin(std::future::ready(result))
2318 }
2319 }
2320
2321 let input = r#"
2322 {
2323 "@context": "https://example.org/context.jsonld",
2324 "@id": "http://example.org/alice",
2325 "@type": "Person",
2326 "name": "Alice",
2327 "email": "alice@example.org"
2328 }"#;
2329
2330 let rule = Rule::new(SK::JsonldDoc);
2331 let (result, _) = crate::parse(rule, input);
2332 let root = result.syntax::<Lang>();
2333
2334 let (turtle, _ctx) = block_on_ready(convert_with_loader(&root, &mut MockLoader, None));
2335 let triples = triples_of(&turtle);
2336
2337 assert_eq!(triples.len(), 3);
2339
2340 let type_triple = triples
2342 .iter()
2343 .find(|t| predicate_iri(t) == Some(RDF_TYPE))
2344 .expect("missing rdf:type triple");
2345 assert_eq!(subject_iri(type_triple), Some("http://example.org/alice"));
2346 assert_eq!(object_iri(type_triple), Some("http://schema.org/Person"));
2347
2348 let name_triple = triples
2350 .iter()
2351 .find(|t| predicate_iri(t) == Some("http://schema.org/name"))
2352 .expect("missing schema:name triple");
2353 assert_eq!(
2354 object_literal(name_triple),
2355 Some(("Alice", None, Some(XSD_STRING)))
2356 );
2357
2358 assert!(
2360 triples
2361 .iter()
2362 .any(|t| predicate_iri(t) == Some("http://schema.org/email")),
2363 "missing schema:email triple"
2364 );
2365 }
2366
2367 #[test]
2368 fn test_prefix_map_populated_from_context() {
2369 let t = parse_jsonld(
2370 r#"{
2371 "@context": {
2372 "foaf": "http://xmlns.com/foaf/0.1/",
2373 "schema": "http://schema.org/"
2374 },
2375 "@id": "http://example.org/bob",
2376 "foaf:name": "Bob"
2377 }"#,
2378 );
2379
2380 let prefix_map: std::collections::HashMap<&str, &str> = t
2381 .prefixes
2382 .iter()
2383 .map(|p| {
2384 (
2385 p.value().prefix.value().as_str(),
2386 match p.value().value.value() {
2387 NamedNode::Full(s, _) => s.as_str(),
2388 _ => "",
2389 },
2390 )
2391 })
2392 .collect();
2393
2394 assert_eq!(
2395 prefix_map.get("foaf"),
2396 Some(&"http://xmlns.com/foaf/0.1/"),
2397 "foaf prefix missing"
2398 );
2399 assert_eq!(
2400 prefix_map.get("schema"),
2401 Some(&"http://schema.org/"),
2402 "schema prefix missing"
2403 );
2404 }
2405
2406 #[test]
2407 fn test_prefix_map_object_term_definition() {
2408 let t = parse_jsonld(
2410 r#"{
2411 "@context": {
2412 "name": { "@id": "http://xmlns.com/foaf/0.1/name" }
2413 },
2414 "@id": "http://example.org/bob",
2415 "name": "Bob"
2416 }"#,
2417 );
2418
2419 let prefix_map: std::collections::HashMap<&str, &str> = t
2420 .prefixes
2421 .iter()
2422 .map(|p| {
2423 (
2424 p.value().prefix.value().as_str(),
2425 match p.value().value.value() {
2426 NamedNode::Full(s, _) => s.as_str(),
2427 _ => "",
2428 },
2429 )
2430 })
2431 .collect();
2432
2433 assert_eq!(
2434 prefix_map.get("name"),
2435 Some(&"http://xmlns.com/foaf/0.1/name"),
2436 "name term missing"
2437 );
2438 }
2439
2440 #[test]
2441 fn test_active_context_returned() {
2442 let (_turtle, ctx) = parse_jsonld_with_context(
2443 r#"{
2444 "@context": {
2445 "@vocab": "http://schema.org/",
2446 "@base": "http://example.org/",
2447 "@language": "en",
2448 "foaf": "http://xmlns.com/foaf/0.1/",
2449 "name": { "@id": "http://xmlns.com/foaf/0.1/name" }
2450 },
2451 "@id": "bob",
2452 "name": "Bob"
2453 }"#,
2454 );
2455
2456 assert_eq!(ctx.vocab.as_deref(), Some("http://schema.org/"));
2457 assert_eq!(ctx.base.as_deref(), Some("http://example.org/"));
2458 assert_eq!(ctx.language.as_deref(), Some("en"));
2459 assert_eq!(
2460 ctx.terms["foaf"].iri.as_deref(),
2461 Some("http://xmlns.com/foaf/0.1/")
2462 );
2463 assert_eq!(
2464 ctx.terms["name"].iri.as_deref(),
2465 Some("http://xmlns.com/foaf/0.1/name")
2466 );
2467 }
2468
2469 fn turtle_base_iri(t: &Turtle) -> Option<&str> {
2472 t.base.as_ref().and_then(|b| {
2473 if let NamedNode::Full(s, _) = &b.value().1.value() {
2474 Some(s.as_str())
2475 } else {
2476 None
2477 }
2478 })
2479 }
2480
2481 #[test]
2482 fn test_base_iri_resolves_relative_subject() {
2483 let (t, _) = parse_jsonld_with_base(
2485 r#"{ "@id": "alice", "http://schema.org/name": "Alice" }"#,
2486 "http://example.org/",
2487 );
2488 let triples = triples_of(&t);
2489 assert_eq!(triples.len(), 1);
2490 assert_eq!(subject_iri(&triples[0]), Some("http://example.org/alice"));
2491 assert_eq!(turtle_base_iri(&t), None);
2493 assert_eq!(t.set_base.as_deref(), Some("http://example.org/"));
2494 }
2495
2496 #[test]
2497 fn test_base_iri_resolves_relative_object_with_type_id() {
2498 let (t, _) = parse_jsonld_with_base(
2499 r#"{
2500 "@context": { "knows": { "@id": "http://schema.org/knows", "@type": "@id" } },
2501 "@id": "http://example.org/alice",
2502 "knows": "bob"
2503 }"#,
2504 "http://example.org/",
2505 );
2506 let triples = triples_of(&t);
2507 assert_eq!(triples.len(), 1);
2508 assert_eq!(object_iri(&triples[0]), Some("http://example.org/bob"));
2509 }
2510
2511 #[test]
2512 fn test_document_base_overrides_base_iri_parameter() {
2513 let (t, ctx) = parse_jsonld_with_base(
2515 r#"{
2516 "@context": { "@base": "http://override.org/" },
2517 "@id": "carol",
2518 "http://schema.org/name": "Carol"
2519 }"#,
2520 "http://example.org/",
2521 );
2522 let triples = triples_of(&t);
2523 assert_eq!(triples.len(), 1);
2524 assert_eq!(subject_iri(&triples[0]), Some("http://override.org/carol"));
2525 assert_eq!(ctx.base.as_deref(), Some("http://override.org/"));
2526 assert_eq!(turtle_base_iri(&t), Some("http://override.org/"));
2528 assert_eq!(t.set_base.as_deref(), Some("http://example.org/"));
2530 }
2531
2532 #[test]
2533 fn test_base_iri_no_effect_on_absolute_iris() {
2534 let (t, _) = parse_jsonld_with_base(
2536 r#"{ "@id": "http://absolute.example/s", "http://schema.org/name": "S" }"#,
2537 "http://example.org/",
2538 );
2539 let triples = triples_of(&t);
2540 assert_eq!(triples.len(), 1);
2541 assert_eq!(subject_iri(&triples[0]), Some("http://absolute.example/s"));
2542 }
2543
2544 #[test]
2545 fn test_document_at_base_without_base_iri_parameter() {
2546 let (t, ctx) = parse_jsonld_with_context(
2548 r#"{
2549 "@context": { "@base": "http://doc.example/" },
2550 "@id": "thing",
2551 "http://schema.org/name": "Thing"
2552 }"#,
2553 );
2554 let triples = triples_of(&t);
2555 assert_eq!(triples.len(), 1);
2556 assert_eq!(subject_iri(&triples[0]), Some("http://doc.example/thing"));
2557 assert_eq!(ctx.base.as_deref(), Some("http://doc.example/"));
2558 assert_eq!(turtle_base_iri(&t), Some("http://doc.example/"));
2559 assert_eq!(t.set_base, None);
2560 }
2561
2562 struct LocalLoader {
2567 entries: Vec<(&'static str, &'static str)>,
2568 }
2569
2570 impl LocalLoader {
2571 fn new(entries: Vec<(&'static str, &'static str)>) -> Self {
2572 Self { entries }
2573 }
2574 }
2575
2576 impl ContextLoader for LocalLoader {
2577 fn load<'a>(
2578 &'a mut self,
2579 url: &'a str,
2580 ) -> Pin<Box<dyn Future<Output = Option<String>> + 'a>> {
2581 let result = self
2582 .entries
2583 .iter()
2584 .find(|(u, _)| *u == url)
2585 .map(|(_, body)| body.to_string());
2586 Box::pin(std::future::ready(result))
2587 }
2588 }
2589
2590 fn parse_with_local_loader(
2591 input: &str,
2592 loader: &mut dyn ContextLoader,
2593 ) -> (Turtle, ActiveContext) {
2594 let rule = Rule::new(SK::JsonldDoc);
2595 let (result, _) = crate::parse(rule, input);
2596 let root = result.syntax::<Lang>();
2597 block_on_ready(convert_with_loader(&root, loader, None))
2598 }
2599
2600 #[test]
2601 fn test_local_context_terms_expanded() {
2602 let mut loader = LocalLoader::new(vec![(
2604 "file:///contexts/person.jsonld",
2605 r#"{ "@context": { "name": "http://schema.org/name", "Person": "http://schema.org/Person" } }"#,
2606 )]);
2607
2608 let (turtle, _) = parse_with_local_loader(
2609 r#"{
2610 "@context": "file:///contexts/person.jsonld",
2611 "@id": "http://example.org/alice",
2612 "@type": "Person",
2613 "name": "Alice"
2614 }"#,
2615 &mut loader,
2616 );
2617 let triples = triples_of(&turtle);
2618 assert_eq!(triples.len(), 2);
2619 assert!(
2620 triples.iter().any(|t| predicate_iri(t) == Some(RDF_TYPE)
2621 && object_iri(t) == Some("http://schema.org/Person")),
2622 "missing rdf:type triple"
2623 );
2624 assert!(
2625 triples
2626 .iter()
2627 .any(|t| predicate_iri(t) == Some("http://schema.org/name")),
2628 "missing schema:name triple"
2629 );
2630 }
2631
2632 #[test]
2633 fn test_local_context_array_merges_multiple_imports() {
2634 let mut loader = LocalLoader::new(vec![(
2636 "file:///contexts/base.jsonld",
2637 r#"{ "@context": { "schema": "http://schema.org/" } }"#,
2638 )]);
2639
2640 let (turtle, ctx) = parse_with_local_loader(
2641 r#"{
2642 "@context": [
2643 "file:///contexts/base.jsonld",
2644 { "name": "schema:name" }
2645 ],
2646 "@id": "http://example.org/bob",
2647 "name": "Bob"
2648 }"#,
2649 &mut loader,
2650 );
2651 let triples = triples_of(&turtle);
2652 assert_eq!(triples.len(), 1);
2653 assert_eq!(predicate_iri(&triples[0]), Some("http://schema.org/name"));
2654 assert_eq!(
2656 ctx.terms["schema"].iri.as_deref(),
2657 Some("http://schema.org/")
2658 );
2659 assert_eq!(
2660 ctx.terms["name"].iri.as_deref(),
2661 Some("http://schema.org/name")
2662 );
2663 }
2664
2665 #[test]
2666 fn test_local_context_unknown_url_is_skipped() {
2667 let mut loader = LocalLoader::new(vec![]); let (turtle, _) = parse_with_local_loader(
2672 r#"{
2673 "@context": "file:///contexts/missing.jsonld",
2674 "@id": "http://example.org/s",
2675 "http://example.org/p": "v"
2676 }"#,
2677 &mut loader,
2678 );
2679 let triples = triples_of(&turtle);
2681 assert_eq!(triples.len(), 1);
2682 assert_eq!(predicate_iri(&triples[0]), Some("http://example.org/p"));
2683 }
2684
2685 #[test]
2686 fn test_local_context_chained_imports() {
2687 let mut loader = LocalLoader::new(vec![
2690 (
2691 "file:///ctx_a.jsonld",
2692 r#"{ "@context": ["file:///ctx_b.jsonld", { "name": "http://schema.org/name" }] }"#,
2693 ),
2694 (
2695 "file:///ctx_b.jsonld",
2696 r#"{ "@context": { "schema": "http://schema.org/" } }"#,
2697 ),
2698 ]);
2699
2700 let (turtle, ctx) = parse_with_local_loader(
2701 r#"{
2702 "@context": "file:///ctx_a.jsonld",
2703 "@id": "http://example.org/carol",
2704 "name": "Carol"
2705 }"#,
2706 &mut loader,
2707 );
2708 let triples = triples_of(&turtle);
2709 assert_eq!(triples.len(), 1);
2710 assert_eq!(predicate_iri(&triples[0]), Some("http://schema.org/name"));
2711 assert_eq!(
2712 ctx.terms["schema"].iri.as_deref(),
2713 Some("http://schema.org/")
2714 );
2715 }
2716
2717 #[test]
2718 fn test_relative_context_url_resolved_against_document_base() {
2719 let rule = Rule::new(SK::JsonldDoc);
2722 let input = r#"{
2723 "@context": "context.jsonld",
2724 "@id": "http://example.org/s",
2725 "name": "Alice"
2726 }"#;
2727 let (result, _) = crate::parse(rule, input);
2728 let root = result.syntax::<Lang>();
2729
2730 let mut loader = LocalLoader::new(vec![(
2731 "http://example.org/context.jsonld",
2732 r#"{ "@context": { "name": "http://schema.org/name" } }"#,
2733 )]);
2734
2735 let (turtle, _) = block_on_ready(convert_with_loader(
2736 &root,
2737 &mut loader,
2738 Some("http://example.org/".to_string()),
2739 ));
2740 let triples = triples_of(&turtle);
2741 assert_eq!(triples.len(), 1);
2742 assert_eq!(predicate_iri(&triples[0]), Some("http://schema.org/name"));
2743 }
2744
2745 #[test]
2746 fn test_relative_context_url_resolved_against_at_base() {
2747 let rule = Rule::new(SK::JsonldDoc);
2750 let input = r#"{
2751 "@context": [
2752 { "@base": "http://other.example/" },
2753 "ctx.jsonld"
2754 ],
2755 "@id": "http://example.org/s",
2756 "label": "Hello"
2757 }"#;
2758 let (result, _) = crate::parse(rule, input);
2759 let root = result.syntax::<Lang>();
2760
2761 let mut loader = LocalLoader::new(vec![(
2762 "http://other.example/ctx.jsonld",
2763 r#"{ "@context": { "label": "http://schema.org/name" } }"#,
2764 )]);
2765
2766 let (turtle, _) = block_on_ready(convert_with_loader(&root, &mut loader, None));
2767 let triples = triples_of(&turtle);
2768 assert_eq!(triples.len(), 1);
2769 assert_eq!(predicate_iri(&triples[0]), Some("http://schema.org/name"));
2770 }
2771
2772 #[test]
2773 fn test_local_context_with_base_iri_parameter() {
2774 let rule = Rule::new(SK::JsonldDoc);
2777 let input = r#"{
2778 "@context": "file:///contexts/vocab.jsonld",
2779 "@id": "dave",
2780 "name": "Dave"
2781 }"#;
2782 let (result, _) = crate::parse(rule, input);
2783 let root = result.syntax::<Lang>();
2784
2785 let mut loader = LocalLoader::new(vec![(
2786 "file:///contexts/vocab.jsonld",
2787 r#"{ "@context": { "name": "http://schema.org/name" } }"#,
2788 )]);
2789
2790 let (turtle, _) = block_on_ready(convert_with_loader(
2791 &root,
2792 &mut loader,
2793 Some("http://example.org/people/".to_string()),
2794 ));
2795
2796 let triples = triples_of(&turtle);
2797 assert_eq!(triples.len(), 1);
2798 assert_eq!(
2799 subject_iri(&triples[0]),
2800 Some("http://example.org/people/dave")
2801 );
2802 assert_eq!(predicate_iri(&triples[0]), Some("http://schema.org/name"));
2803 }
2804
2805 fn format_jsonld(input: &str, width: usize) -> String {
2808 use super::super::parser::format;
2809 use crate::parse as crate_parse;
2810 let rule = Rule::new(SK::JsonldDoc);
2811 let (result, _) = crate_parse(rule, input);
2812 let root = result.syntax::<Lang>();
2813 format::format(&root, width)
2814 }
2815
2816 #[test]
2817 fn format_small_object_fits_on_one_line() {
2818 let input = r#"{"a": 1, "b": 2}"#;
2821 let out = format_jsonld(input, 80);
2822 assert_eq!(out, "{ \"a\": 1, \"b\": 2 }\n");
2823 }
2824
2825 #[test]
2826 fn format_large_object_breaks() {
2827 let input = r#"{"key": "value"}"#;
2829 let out = format_jsonld(input, 10);
2830 assert_eq!(out, "{\n \"key\": \"value\"\n}\n");
2831 }
2832
2833 #[test]
2834 fn format_multiple_members_break() {
2835 let input = r#"{"a":1,"b":2,"c":3}"#;
2836 let out = format_jsonld(input, 10);
2837 assert_eq!(out, "{\n \"a\": 1,\n \"b\": 2,\n \"c\": 3\n}\n");
2838 }
2839
2840 #[test]
2841 fn format_nested_objects() {
2842 let input = r#"{"outer":{"inner":1}}"#;
2844 let out = format_jsonld(input, 80);
2845 assert_eq!(out, "{ \"outer\": { \"inner\": 1 } }\n");
2846 }
2847
2848 #[test]
2849 fn format_is_idempotent() {
2850 let input = r#"{"key": "value", "num": 42}"#;
2852 let first = format_jsonld(input, 80);
2853 let second = format_jsonld(&first, 80);
2854 assert_eq!(first, second);
2855 }
2856}