shifty_parse/
sparql_path.rs1use crate::graph::Loaded;
19use crate::vocab;
20use oxrdf::NamedNode;
21use shifty_algebra::Path;
22
23pub fn parse_property_path(expr: &str, shapes: &Loaded) -> Result<Path, String> {
27 let tokens = lex(expr)?;
28 let mut parser = Parser {
29 tokens: &tokens,
30 pos: 0,
31 prefixes: &shapes.prefixes,
32 };
33 let path = parser.parse_path_alternative()?;
34 if parser.pos != parser.tokens.len() {
35 return Err(format!(
36 "unexpected trailing input in property path {expr:?} at token {}",
37 parser.pos
38 ));
39 }
40 Ok(path)
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44enum Token {
45 AbsoluteIri(String),
47 PrefixedName(String),
49 A,
50 Caret,
51 Slash,
52 Pipe,
53 Star,
54 Plus,
55 Question,
56 Bang,
57 LParen,
58 RParen,
59}
60
61fn lex(expr: &str) -> Result<Vec<Token>, String> {
62 let mut tokens = Vec::new();
63 let mut chars = expr.char_indices().peekable();
64 while let Some(&(i, c)) = chars.peek() {
65 match c {
66 c if c.is_whitespace() => {
67 chars.next();
68 }
69 '^' => {
70 tokens.push(Token::Caret);
71 chars.next();
72 }
73 '/' => {
74 tokens.push(Token::Slash);
75 chars.next();
76 }
77 '|' => {
78 tokens.push(Token::Pipe);
79 chars.next();
80 }
81 '*' => {
82 tokens.push(Token::Star);
83 chars.next();
84 }
85 '+' => {
86 tokens.push(Token::Plus);
87 chars.next();
88 }
89 '?' => {
90 tokens.push(Token::Question);
91 chars.next();
92 }
93 '!' => {
94 tokens.push(Token::Bang);
95 chars.next();
96 }
97 '(' => {
98 tokens.push(Token::LParen);
99 chars.next();
100 }
101 ')' => {
102 tokens.push(Token::RParen);
103 chars.next();
104 }
105 '<' => {
106 chars.next();
107 let start = i + 1;
108 let mut end = expr.len();
109 let mut closed = false;
110 for (j, c2) in chars.by_ref() {
111 if c2 == '>' {
112 end = j;
113 closed = true;
114 break;
115 }
116 }
117 if !closed {
118 return Err(format!("unterminated <IRI> in property path {expr:?}"));
119 }
120 tokens.push(Token::AbsoluteIri(expr[start..end].to_string()));
121 }
122 c if is_name_char(c) => {
123 let start = i;
124 let mut end = expr.len();
125 while let Some(&(j, c2)) = chars.peek() {
126 if is_name_char(c2) {
127 chars.next();
128 } else {
129 end = j;
130 break;
131 }
132 }
133 let word = &expr[start..end];
134 if word == "a" {
135 tokens.push(Token::A);
136 } else if word.contains(':') {
137 tokens.push(Token::PrefixedName(word.to_string()));
138 } else {
139 return Err(format!(
140 "expected 'prefix:local' or 'a' in property path, got {word:?}"
141 ));
142 }
143 }
144 other => {
145 return Err(format!(
146 "unexpected character {other:?} in property path {expr:?}"
147 ));
148 }
149 }
150 }
151 Ok(tokens)
152}
153
154fn is_name_char(c: char) -> bool {
155 c.is_alphanumeric() || matches!(c, '_' | '-' | '.' | ':')
156}
157
158struct Parser<'a> {
159 tokens: &'a [Token],
160 pos: usize,
161 prefixes: &'a [(String, String)],
162}
163
164impl Parser<'_> {
165 fn peek(&self) -> Option<&Token> {
166 self.tokens.get(self.pos)
167 }
168
169 fn eat(&mut self, token: &Token) -> bool {
170 if self.peek() == Some(token) {
171 self.pos += 1;
172 true
173 } else {
174 false
175 }
176 }
177
178 fn expect(&mut self, token: Token) -> Result<(), String> {
179 if self.eat(&token) {
180 Ok(())
181 } else {
182 Err(format!(
183 "expected {token:?} in property path, got {:?}",
184 self.peek()
185 ))
186 }
187 }
188
189 fn parse_path_alternative(&mut self) -> Result<Path, String> {
191 let mut parts = vec![self.parse_path_sequence()?];
192 while self.eat(&Token::Pipe) {
193 parts.push(self.parse_path_sequence()?);
194 }
195 Ok(Path::alt(parts))
196 }
197
198 fn parse_path_sequence(&mut self) -> Result<Path, String> {
200 let mut parts = vec![self.parse_path_elt_or_inverse()?];
201 while self.eat(&Token::Slash) {
202 parts.push(self.parse_path_elt_or_inverse()?);
203 }
204 Ok(Path::seq(parts))
205 }
206
207 fn parse_path_elt_or_inverse(&mut self) -> Result<Path, String> {
209 if self.eat(&Token::Caret) {
210 Ok(self.parse_path_elt()?.inverse())
211 } else {
212 self.parse_path_elt()
213 }
214 }
215
216 fn parse_path_elt(&mut self) -> Result<Path, String> {
218 let primary = self.parse_path_primary()?;
219 if self.eat(&Token::Star) {
220 Ok(primary.star())
221 } else if self.eat(&Token::Plus) {
222 Ok(primary.one_or_more())
223 } else if self.eat(&Token::Question) {
224 Ok(primary.zero_or_one())
225 } else {
226 Ok(primary)
227 }
228 }
229
230 fn parse_path_primary(&mut self) -> Result<Path, String> {
232 match self.tokens.get(self.pos).cloned() {
233 Some(Token::AbsoluteIri(iri)) => {
234 self.pos += 1;
235 let named =
236 NamedNode::new(&iri).map_err(|e| format!("invalid IRI <{iri}>: {e}"))?;
237 Ok(Path::Pred(named))
238 }
239 Some(Token::PrefixedName(name)) => {
240 self.pos += 1;
241 let named = resolve_prefixed_name(&name, self.prefixes)?;
242 Ok(Path::Pred(named))
243 }
244 Some(Token::A) => {
245 self.pos += 1;
246 Ok(Path::Pred(vocab::rdf_type()))
247 }
248 Some(Token::LParen) => {
249 self.pos += 1;
250 let inner = self.parse_path_alternative()?;
251 self.expect(Token::RParen)?;
252 Ok(inner)
253 }
254 Some(Token::Bang) => Err(
255 "negated property sets ('!...') have no equivalent in the path algebra".to_string(),
256 ),
257 other => Err(format!("unexpected token {other:?} in property path")),
258 }
259 }
260}
261
262fn resolve_prefixed_name(token: &str, prefixes: &[(String, String)]) -> Result<NamedNode, String> {
268 let (prefix, local) = token
269 .split_once(':')
270 .expect("PrefixedName tokens always contain ':'");
271 let (_, namespace) = prefixes
272 .iter()
273 .find(|(p, _)| p == prefix)
274 .ok_or_else(|| format!("undeclared prefix {prefix:?} in property path {token:?}"))?;
275 NamedNode::new(format!("{namespace}{local}"))
276 .map_err(|e| format!("invalid IRI from {token:?}: {e}"))
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::graph::Loaded;
283
284 fn shapes_with_prefixes() -> Loaded {
285 Loaded::from_turtle(
286 b"@prefix ex: <http://ex/> . @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . ex:s ex:p ex:o .",
287 None,
288 )
289 .unwrap()
290 }
291
292 fn pred(iri: &str) -> Path {
293 Path::Pred(NamedNode::new(iri).unwrap())
294 }
295
296 #[test]
297 fn single_predicate() {
298 let shapes = shapes_with_prefixes();
299 assert_eq!(
300 parse_property_path("ex:p", &shapes).unwrap(),
301 pred("http://ex/p")
302 );
303 }
304
305 #[test]
306 fn absolute_iri() {
307 let shapes = shapes_with_prefixes();
308 assert_eq!(
309 parse_property_path("<http://ex/p>", &shapes).unwrap(),
310 pred("http://ex/p")
311 );
312 }
313
314 #[test]
315 fn sequence() {
316 let shapes = shapes_with_prefixes();
317 assert_eq!(
318 parse_property_path("ex:p/rdfs:label", &shapes).unwrap(),
319 Path::Seq(vec![
320 pred("http://ex/p"),
321 pred("http://www.w3.org/2000/01/rdf-schema#label")
322 ])
323 );
324 }
325
326 #[test]
327 fn inverse() {
328 let shapes = shapes_with_prefixes();
329 assert_eq!(
330 parse_property_path("^ex:p", &shapes).unwrap(),
331 pred("http://ex/p").inverse()
332 );
333 }
334
335 #[test]
336 fn inverse_then_sequence() {
337 let shapes = shapes_with_prefixes();
338 assert_eq!(
339 parse_property_path("^ex:p/rdfs:label", &shapes).unwrap(),
340 Path::Seq(vec![
341 pred("http://ex/p").inverse(),
342 pred("http://www.w3.org/2000/01/rdf-schema#label")
343 ])
344 );
345 }
346
347 #[test]
348 fn alternation() {
349 let shapes = shapes_with_prefixes();
350 assert_eq!(
351 parse_property_path("ex:p|ex:q", &shapes).unwrap(),
352 Path::Alt(vec![pred("http://ex/p"), pred("http://ex/q")])
353 );
354 }
355
356 #[test]
357 fn zero_or_more() {
358 let shapes = shapes_with_prefixes();
359 assert_eq!(
360 parse_property_path("ex:p*", &shapes).unwrap(),
361 pred("http://ex/p").star()
362 );
363 }
364
365 #[test]
366 fn one_or_more() {
367 let shapes = shapes_with_prefixes();
368 assert_eq!(
369 parse_property_path("ex:p+", &shapes).unwrap(),
370 pred("http://ex/p").one_or_more()
371 );
372 }
373
374 #[test]
375 fn zero_or_one() {
376 let shapes = shapes_with_prefixes();
377 assert_eq!(
378 parse_property_path("ex:p?", &shapes).unwrap(),
379 pred("http://ex/p").zero_or_one()
380 );
381 }
382
383 #[test]
384 fn grouping_changes_precedence() {
385 let shapes = shapes_with_prefixes();
386 assert_eq!(
388 parse_property_path("ex:p/(ex:q|ex:r)", &shapes).unwrap(),
389 Path::Seq(vec![
390 pred("http://ex/p"),
391 Path::Alt(vec![pred("http://ex/q"), pred("http://ex/r")]),
392 ])
393 );
394 }
395
396 #[test]
397 fn a_keyword_is_rdf_type() {
398 let shapes = shapes_with_prefixes();
399 assert_eq!(
400 parse_property_path("a", &shapes).unwrap(),
401 pred("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
402 );
403 }
404
405 #[test]
406 fn unknown_prefix_is_an_error() {
407 let shapes = shapes_with_prefixes();
408 assert!(parse_property_path("nope:p", &shapes).is_err());
409 }
410
411 #[test]
412 fn negated_property_set_is_rejected() {
413 let shapes = shapes_with_prefixes();
414 let err = parse_property_path("!ex:p", &shapes).unwrap_err();
415 assert!(err.contains("negated property set"), "{err}");
416 }
417
418 #[test]
419 fn trailing_garbage_is_an_error() {
420 let shapes = shapes_with_prefixes();
421 assert!(parse_property_path("ex:p )", &shapes).is_err());
422 }
423}