Skip to main content

shifty_parse/
graph.rs

1//! Loading an RDF graph from Turtle and convenience accessors over it.
2
3use crate::diagnostics::ParseError;
4use crate::vocab;
5use oxrdf::{Graph, NamedNode, NamedNodeRef, NamedOrBlankNode, Term};
6use oxttl::{NTriplesParser, TurtleParser};
7use std::collections::HashSet;
8use std::fs::File;
9use std::io::{BufReader, Read};
10use std::path::Path;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RdfFormat {
14    Turtle,
15    NTriples,
16}
17
18/// A loaded shapes graph plus the prefixes declared in the document.
19pub struct Loaded {
20    pub graph: Graph,
21    pub prefixes: Vec<(String, String)>,
22    pub base: Option<String>,
23}
24
25impl Loaded {
26    /// Parse a Turtle document into an in-memory graph.
27    pub fn from_turtle(data: &[u8], base: Option<&str>) -> Result<Self, ParseError> {
28        Self::from_turtle_reader(data, base)
29    }
30
31    pub fn from_ntriples(data: &[u8]) -> Result<Self, ParseError> {
32        Self::from_ntriples_reader(data)
33    }
34
35    pub fn from_path(
36        path: &Path,
37        format: RdfFormat,
38        base: Option<&str>,
39    ) -> Result<Self, ParseError> {
40        let file = File::open(path)
41            .map_err(|e| ParseError(format!("failed to open {}: {e}", path.display())))?;
42        let reader = BufReader::new(file);
43        match format {
44            RdfFormat::Turtle => Self::from_turtle_reader(reader, base),
45            RdfFormat::NTriples => Self::from_ntriples_reader(reader),
46        }
47    }
48
49    fn from_turtle_reader(reader: impl Read, base: Option<&str>) -> Result<Self, ParseError> {
50        let mut parser = TurtleParser::new();
51        if let Some(b) = base {
52            parser = parser
53                .with_base_iri(b)
54                .map_err(|e| ParseError(format!("invalid base IRI: {e}")))?;
55        }
56        let mut reader = parser.for_reader(reader);
57        let mut graph = Graph::new();
58        for triple in reader.by_ref() {
59            let triple = triple.map_err(|e| ParseError(format!("turtle syntax error: {e}")))?;
60            graph.insert(&triple);
61        }
62        let prefixes = reader
63            .prefixes()
64            .map(|(p, iri)| (p.to_string(), iri.to_string()))
65            .collect();
66        let base = reader.base_iri().map(|s| s.to_string());
67        Ok(Self {
68            graph,
69            prefixes,
70            base,
71        })
72    }
73
74    fn from_ntriples_reader(reader: impl Read) -> Result<Self, ParseError> {
75        let mut graph = Graph::new();
76        for triple in NTriplesParser::new().for_reader(reader) {
77            let triple = triple.map_err(|e| ParseError(format!("N-Triples syntax error: {e}")))?;
78            graph.insert(&triple);
79        }
80        Ok(Self {
81            graph,
82            prefixes: Vec::new(),
83            base: None,
84        })
85    }
86
87    /// All objects of `(subject, predicate)`.
88    pub fn objects(&self, subject: &NamedOrBlankNode, predicate: NamedNodeRef) -> Vec<Term> {
89        self.graph
90            .objects_for_subject_predicate(subject, predicate)
91            .map(|t| t.into_owned())
92            .collect()
93    }
94
95    /// The first object of `(subject, predicate)`, if any.
96    pub fn object(&self, subject: &NamedOrBlankNode, predicate: NamedNodeRef) -> Option<Term> {
97        self.graph
98            .object_for_subject_predicate(subject, predicate)
99            .map(|t| t.into_owned())
100    }
101
102    /// Does the subject have `(subject, rdf:type, ty)`?
103    pub fn has_type(&self, subject: &NamedOrBlankNode, ty: NamedNodeRef) -> bool {
104        self.objects(subject, vocab::RDF_TYPE)
105            .iter()
106            .any(|t| matches!(t, Term::NamedNode(n) if n.as_ref() == ty))
107    }
108
109    /// Does the subject have `ty` through `rdf:type/rdfs:subClassOf*`?
110    pub fn is_instance_of(&self, subject: &NamedOrBlankNode, ty: NamedNodeRef) -> bool {
111        let mut pending: Vec<NamedNode> = self
112            .objects(subject, vocab::RDF_TYPE)
113            .into_iter()
114            .filter_map(|term| match term {
115                Term::NamedNode(node) => Some(node),
116                _ => None,
117            })
118            .collect();
119        let mut seen = HashSet::new();
120        while let Some(class) = pending.pop() {
121            if class.as_ref() == ty {
122                return true;
123            }
124            if !seen.insert(class.clone()) {
125                continue;
126            }
127            pending.extend(
128                self.objects(&NamedOrBlankNode::NamedNode(class), vocab::RDFS_SUBCLASSOF)
129                    .into_iter()
130                    .filter_map(|term| match term {
131                        Term::NamedNode(node) => Some(node),
132                        _ => None,
133                    }),
134            );
135        }
136        false
137    }
138
139    /// Merge all triples from `other` into this graph.
140    pub fn merge_from(&mut self, other: &Loaded) {
141        for triple in other.graph.iter() {
142            self.graph.insert(triple);
143        }
144    }
145
146    /// Read an `rdf:List` starting at `head` into its member terms.
147    pub fn read_list(&self, head: &Term) -> Vec<Term> {
148        let mut out = Vec::new();
149        let mut cursor = head.clone();
150        while let Some(node) = term_to_node(&cursor) {
151            if is_nil(&cursor) {
152                break;
153            }
154            if let Some(first) = self.object(&node, vocab::RDF_FIRST) {
155                out.push(first);
156            }
157            match self.object(&node, vocab::RDF_REST) {
158                Some(rest) => cursor = rest,
159                None => break,
160            }
161        }
162        out
163    }
164}
165
166/// Convert an object term into a subject position node, if it is not a literal.
167pub fn term_to_node(term: &Term) -> Option<NamedOrBlankNode> {
168    match term {
169        Term::NamedNode(n) => Some(NamedOrBlankNode::NamedNode(n.clone())),
170        Term::BlankNode(b) => Some(NamedOrBlankNode::BlankNode(b.clone())),
171        Term::Literal(_) => None,
172    }
173}
174
175/// Is this term `rdf:nil`?
176pub fn is_nil(term: &Term) -> bool {
177    matches!(term, Term::NamedNode(n) if n.as_ref() == vocab::RDF_NIL)
178}
179
180/// The IRI of a named node, or `None` for blanks/literals.
181pub fn as_named(term: &Term) -> Option<NamedNode> {
182    match term {
183        Term::NamedNode(n) => Some(n.clone()),
184        _ => None,
185    }
186}