Skip to main content

rdf_parsers/ntriples/
convert.rs

1use rowan::SyntaxNode;
2
3use crate::Spanned;
4
5use super::parser::{Lang, SyntaxKind};
6use crate::model::*;
7
8type Node = SyntaxNode<Lang>;
9
10fn text_range(node: &Node) -> std::ops::Range<usize> {
11    let r = node.text_range();
12    r.start().into()..r.end().into()
13}
14
15fn child(node: &Node, kind: SyntaxKind) -> Option<Node> {
16    node.children().find(|c| c.kind() == kind)
17}
18
19fn children(node: &Node, kind: SyntaxKind) -> impl Iterator<Item = Node> {
20    node.children().filter(move |c| c.kind() == kind)
21}
22
23fn terminal_text(node: &Node) -> String {
24    node.text().to_string()
25}
26
27pub fn convert(root: &Node) -> Turtle {
28    let triples: Vec<Spanned<Triple>> = children(root, SyntaxKind::Triple)
29        .map(|t| {
30            let range = text_range(&t);
31            Spanned(convert_triple(&t), range)
32        })
33        .collect();
34
35    Turtle::new(None, Vec::new(), triples)
36}
37
38fn convert_triple(node: &Node) -> Triple {
39    let subject = child(node, SyntaxKind::Subject)
40        .map(|s| {
41            let range = text_range(&s);
42            Spanned(convert_subject(&s), range)
43        })
44        .unwrap_or_else(|| Spanned(Term::Invalid, text_range(node)));
45
46    let predicate = child(node, SyntaxKind::Predicate)
47        .map(|p| {
48            let range = text_range(&p);
49            Spanned(convert_predicate(&p), range)
50        })
51        .unwrap_or_else(|| Spanned(Term::Invalid, text_range(node)));
52
53    let object = child(node, SyntaxKind::Object)
54        .map(|o| {
55            let range = text_range(&o);
56            Spanned(convert_object(&o), range)
57        })
58        .unwrap_or_else(|| Spanned(Term::Invalid, text_range(node)));
59
60    let po_range = predicate.span().start..object.span().end;
61    let po = Spanned(
62        PO {
63            predicate,
64            object: vec![object],
65        },
66        po_range,
67    );
68
69    Triple {
70        subject,
71        po: vec![po],
72        graph: None,
73    }
74}
75
76fn convert_subject(node: &Node) -> Term {
77    if let Some(iriref) = child(node, SyntaxKind::Iriref) {
78        Term::NamedNode(iri_from_iriref_node(&iriref))
79    } else if let Some(bnode) = child(node, SyntaxKind::BlankNodeLabel) {
80        Term::BlankNode(blank_node_from_label(&bnode))
81    } else {
82        Term::Invalid
83    }
84}
85
86fn convert_predicate(node: &Node) -> Term {
87    if let Some(iriref) = child(node, SyntaxKind::Iriref) {
88        Term::NamedNode(iri_from_iriref_node(&iriref))
89    } else {
90        Term::Invalid
91    }
92}
93
94fn convert_object(node: &Node) -> Term {
95    if let Some(iriref) = child(node, SyntaxKind::Iriref) {
96        Term::NamedNode(iri_from_iriref_node(&iriref))
97    } else if let Some(bnode) = child(node, SyntaxKind::BlankNodeLabel) {
98        Term::BlankNode(blank_node_from_label(&bnode))
99    } else if let Some(lit) = child(node, SyntaxKind::Literal) {
100        Term::Literal(convert_literal(&lit))
101    } else {
102        Term::Invalid
103    }
104}
105
106fn iri_from_iriref_node(node: &Node) -> NamedNode {
107    let text = terminal_text(node);
108    let offset: usize = node.text_range().start().into();
109    let inner = text.trim_start_matches('<').trim_end_matches('>');
110    NamedNode::Full(inner.to_string(), offset)
111}
112
113fn blank_node_from_label(node: &Node) -> BlankNode {
114    let text = terminal_text(node);
115    let offset: usize = node.text_range().start().into();
116    let name = text.strip_prefix("_:").unwrap_or(&text);
117    BlankNode::Named(name.to_string(), offset)
118}
119
120fn convert_literal(node: &Node) -> Literal {
121    let offset: usize = node.text_range().start().into();
122
123    let (value, len) = if let Some(str_node) = child(node, SyntaxKind::StringLiteralQuote) {
124        let text = terminal_text(&str_node);
125        let inner = text
126            .strip_prefix('"')
127            .and_then(|s| s.strip_suffix('"'))
128            .unwrap_or(&text);
129        (inner.to_string(), 1)
130    } else {
131        (String::new(), 0)
132    };
133
134    let lang = child(node, SyntaxKind::Langtag).map(|n| {
135        let span = text_range(&n);
136        let text = terminal_text(&n);
137        let value = text.strip_prefix('@').unwrap_or(&text).to_string();
138        Spanned(value, span)
139    });
140
141    let ty =
142        child(node, SyntaxKind::Iriref).map(|n| Spanned(iri_from_iriref_node(&n), text_range(&n)));
143
144    Literal::RDF(RDFLiteral {
145        value,
146        quote_style: StringStyle::Double,
147        lang,
148        ty,
149        idx: offset,
150        len,
151    })
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::{ntriples::parser as lang, parse as crate_parse};
158
159    fn parse(input: &str) -> Turtle {
160        let (result, _) = crate_parse(lang::Rule::new(lang::SyntaxKind::NtriplesDoc), input);
161        let root = result.syntax::<lang::Lang>();
162        convert(&root)
163    }
164
165    fn nn_eq(a: &NamedNode, b: &NamedNode) -> bool {
166        match (a, b) {
167            (NamedNode::Full(s1, _), NamedNode::Full(s2, _)) => s1 == s2,
168            _ => false,
169        }
170    }
171
172    fn term_nn(t: &Term) -> &NamedNode {
173        match t {
174            Term::NamedNode(nn) => nn,
175            other => panic!("expected NamedNode, got {:?}", other),
176        }
177    }
178
179    fn term_bn(t: &Term) -> &BlankNode {
180        match t {
181            Term::BlankNode(bn) => bn,
182            other => panic!("expected BlankNode, got {:?}", other),
183        }
184    }
185
186    fn term_lit(t: &Term) -> &Literal {
187        match t {
188            Term::Literal(l) => l,
189            other => panic!("expected Literal, got {:?}", other),
190        }
191    }
192
193    #[test]
194    fn simple_triple_with_iris() {
195        let doc = parse(
196            "<http://example.org/alice> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .",
197        );
198        assert_eq!(doc.triples.len(), 1);
199        let t = doc.triples[0].value();
200        assert!(nn_eq(
201            term_nn(t.subject.value()),
202            &NamedNode::Full("http://example.org/alice".to_string(), 0)
203        ));
204        assert_eq!(t.po.len(), 1);
205        assert!(nn_eq(
206            term_nn(t.po[0].predicate.value()),
207            &NamedNode::Full(
208                "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
209                0
210            )
211        ));
212        assert!(nn_eq(
213            term_nn(t.po[0].object[0].value()),
214            &NamedNode::Full("http://xmlns.com/foaf/0.1/Person".to_string(), 0)
215        ));
216    }
217
218    #[test]
219    fn blank_node_as_subject() {
220        let doc = parse("_:b1 <http://example.org/name> <http://example.org/value> .");
221        assert_eq!(doc.triples.len(), 1);
222        let t = doc.triples[0].value();
223        let bn = term_bn(t.subject.value());
224        assert!(matches!(bn, BlankNode::Named(name, _) if name == "b1"));
225    }
226
227    #[test]
228    fn blank_node_as_object() {
229        let doc = parse("<http://example.org/alice> <http://example.org/knows> _:b2 .");
230        assert_eq!(doc.triples.len(), 1);
231        let t = doc.triples[0].value();
232        let bn = term_bn(t.po[0].object[0].value());
233        assert!(matches!(bn, BlankNode::Named(name, _) if name == "b2"));
234    }
235
236    #[test]
237    fn string_literal_object() {
238        let doc = parse("<http://example.org/alice> <http://example.org/name> \"Alice\" .");
239        assert_eq!(doc.triples.len(), 1);
240        let t = doc.triples[0].value();
241        let lit = term_lit(t.po[0].object[0].value());
242        match lit {
243            Literal::RDF(rdf) => {
244                assert_eq!(rdf.value, "Alice");
245                assert_eq!(rdf.quote_style, StringStyle::Double);
246                assert!(rdf.lang.is_none());
247                assert!(rdf.ty.is_none());
248            }
249            other => panic!("expected RDF literal, got {:?}", other),
250        }
251    }
252
253    #[test]
254    fn literal_with_language_tag() {
255        let doc = parse("<http://example.org/alice> <http://example.org/name> \"Alice\"@en .");
256        assert_eq!(doc.triples.len(), 1);
257        let t = doc.triples[0].value();
258        let lit = term_lit(t.po[0].object[0].value());
259        match lit {
260            Literal::RDF(rdf) => {
261                assert_eq!(rdf.value, "Alice");
262                assert_eq!(rdf.lang.as_ref().map(|l| l.as_str()), Some("en"));
263                assert!(rdf.ty.is_none());
264            }
265            other => panic!("expected RDF literal, got {:?}", other),
266        }
267    }
268
269    #[test]
270    fn literal_with_datatype() {
271        let doc = parse(
272            "<http://example.org/alice> <http://example.org/age> \"30\"^^<http://www.w3.org/2001/XMLSchema#integer> .",
273        );
274        assert_eq!(doc.triples.len(), 1);
275        let t = doc.triples[0].value();
276        let lit = term_lit(t.po[0].object[0].value());
277        match lit {
278            Literal::RDF(rdf) => {
279                assert_eq!(rdf.value, "30");
280                assert!(rdf.lang.is_none());
281                let ty = rdf.ty.as_ref().expect("datatype should be present");
282                assert!(nn_eq(
283                    ty,
284                    &NamedNode::Full("http://www.w3.org/2001/XMLSchema#integer".to_string(), 0)
285                ));
286            }
287            other => panic!("expected RDF literal, got {:?}", other),
288        }
289    }
290
291    #[test]
292    fn multiple_triples() {
293        let doc = parse(concat!(
294            "<http://example.org/alice> <http://example.org/name> \"Alice\" .\n",
295            "<http://example.org/bob> <http://example.org/name> \"Bob\" .\n",
296        ));
297        assert_eq!(doc.triples.len(), 2);
298
299        let t0 = doc.triples[0].value();
300        assert!(nn_eq(
301            term_nn(t0.subject.value()),
302            &NamedNode::Full("http://example.org/alice".to_string(), 0)
303        ));
304
305        let t1 = doc.triples[1].value();
306        assert!(nn_eq(
307            term_nn(t1.subject.value()),
308            &NamedNode::Full("http://example.org/bob".to_string(), 0)
309        ));
310    }
311
312    // ── fault-tolerant parsing ────────────────────────────────────────────────
313
314    fn parse_raw(input: &str) -> crate::Parse {
315        let (result, _) = crate_parse(lang::Rule::new(lang::SyntaxKind::NtriplesDoc), input);
316        result
317    }
318
319    #[test]
320    fn test_valid_input_has_no_errors() {
321        let p = parse_raw("<http://example.org/s> <http://example.org/p> <http://example.org/o> .");
322        assert_eq!(p.errors.len(), 0, "valid input should produce no errors");
323    }
324
325    #[test]
326    fn test_missing_trailing_dot_reports_error() {
327        let p = parse_raw("<http://example.org/s> <http://example.org/p> <http://example.org/o>");
328        assert!(
329            p.errors.len() > 0,
330            "missing trailing dot should produce an error"
331        );
332        assert!(
333            p.errors.iter().any(|e| e.contains("Stop")),
334            "expected an error mentioning Stop, got: {:?}",
335            p.errors.iter().collect::<Vec<_>>()
336        );
337    }
338
339    #[test]
340    fn no_prefixes_and_no_base() {
341        let doc = parse("<http://example.org/s> <http://example.org/p> <http://example.org/o> .");
342        assert!(doc.base.is_none());
343        assert!(doc.prefixes.is_empty());
344    }
345
346    // ── incremental re-parse tests ────────────────────────────────────────────
347    //
348    // These tests verify that the A* search finds the correct parse within its
349    // iteration budget after a small edit.  They catch grammar weight errors:
350    // if a token's error_value is set too high the heuristic becomes too
351    // optimistic and the search exhausts its budget before finding the solution.
352
353    use crate::{IncrementalBias, PrevParseInfo, TokenTrait, parse_incremental};
354
355    fn prev_info_incr(text: &str) -> PrevParseInfo {
356        let (_, tokens) = crate_parse(lang::Rule::new(lang::SyntaxKind::NtriplesDoc), text);
357        let mut depth: i32 = 0;
358        PrevParseInfo {
359            tokens: tokens
360                .iter()
361                .map(|t| {
362                    let d = depth.clamp(0, 255) as u8;
363                    depth += t.kind.bracket_delta() as i32;
364                    t.to_prev_token(d)
365                })
366                .collect(),
367            had_errors: false,
368        }
369    }
370
371    fn parse_incr(before: &str, after: &str) -> Turtle {
372        let prev = prev_info_incr(before);
373        let (result, _) = parse_incremental(
374            lang::Rule::new(lang::SyntaxKind::NtriplesDoc),
375            after,
376            Some(&prev),
377            IncrementalBias::default(),
378        );
379        let root = result.syntax::<lang::Lang>();
380        convert(&root)
381    }
382
383    /// Remove the object: `<a> <b> <c> .` → `<a> <b> .`
384    /// The parser should recover and produce one triple with the original subject.
385    #[test]
386    fn test_incremental_remove_object() {
387        let doc = parse_incr(
388            "<http://a> <http://b> <http://c> .",
389            "<http://a> <http://b> .",
390        );
391        assert_eq!(doc.triples.len(), 1, "should produce one triple");
392        match doc.triples[0].value().subject.value() {
393            Term::NamedNode(NamedNode::Full(iri, _)) => {
394                assert_eq!(iri, "http://a")
395            }
396            other => panic!("expected http://a as subject, got {:?}", other),
397        }
398    }
399
400    /// Add a second triple: `<a> <b> <c> .` → `<a> <b> <c> .\n<d> <e> <f> .`
401    /// The incremental parser should produce two triples with the correct subjects.
402    #[test]
403    fn test_incremental_add_triple() {
404        let doc = parse_incr(
405            "<http://a> <http://b> <http://c> .",
406            "<http://a> <http://b> <http://c> .\n<http://d> <http://e> <http://f> .",
407        );
408        assert_eq!(doc.triples.len(), 2, "should produce two triples");
409        match doc.triples[0].value().subject.value() {
410            Term::NamedNode(NamedNode::Full(iri, _)) => assert_eq!(iri, "http://a"),
411            other => panic!("expected http://a as first subject, got {:?}", other),
412        }
413        match doc.triples[1].value().subject.value() {
414            Term::NamedNode(NamedNode::Full(iri, _)) => assert_eq!(iri, "http://d"),
415            other => panic!("expected http://d as second subject, got {:?}", other),
416        }
417    }
418
419    /// Change the subject: `<a> <b> <c> .` → `<x> <b> <c> .`
420    /// Only one token changes; the incremental bias should preserve the other roles.
421    #[test]
422    fn test_incremental_change_subject() {
423        let doc = parse_incr(
424            "<http://a> <http://b> <http://c> .",
425            "<http://x> <http://b> <http://c> .",
426        );
427        assert_eq!(doc.triples.len(), 1, "should produce one triple");
428        match doc.triples[0].value().subject.value() {
429            Term::NamedNode(NamedNode::Full(iri, _)) => assert_eq!(iri, "http://x"),
430            other => panic!("expected http://x as subject, got {:?}", other),
431        }
432        match doc.triples[0].value().po[0].predicate.value() {
433            Term::NamedNode(NamedNode::Full(iri, _)) => assert_eq!(iri, "http://b"),
434            other => panic!("expected http://b as predicate, got {:?}", other),
435        }
436    }
437}