1#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum RegularPathExpr {
15 Label(String),
17 Concat(Box<RegularPathExpr>, Box<RegularPathExpr>),
19 Alternation(Box<RegularPathExpr>, Box<RegularPathExpr>),
21 KleeneStar(Box<RegularPathExpr>),
23 Bounded {
25 inner: Box<RegularPathExpr>,
26 min: u32,
27 max: u32,
28 },
29}
30
31impl RegularPathExpr {
32 pub fn label(name: impl Into<String>) -> Self {
33 Self::Label(name.into())
34 }
35 pub fn concat(left: Self, right: Self) -> Self {
36 Self::Concat(Box::new(left), Box::new(right))
37 }
38 pub fn alt(left: Self, right: Self) -> Self {
39 Self::Alternation(Box::new(left), Box::new(right))
40 }
41 pub fn star(inner: Self) -> Self {
42 Self::KleeneStar(Box::new(inner))
43 }
44 pub fn bounded(inner: Self, min: u32, max: u32) -> Self {
45 Self::Bounded {
46 inner: Box::new(inner),
47 min,
48 max,
49 }
50 }
51}
52
53#[derive(Debug, thiserror::Error, PartialEq, Eq)]
54pub enum RPQParseError {
55 #[error("unexpected token at position {position}: {token:?}")]
56 Unexpected { position: usize, token: String },
57 #[error("unexpected end of expression")]
58 Eof,
59 #[error("missing closing parenthesis")]
60 MissingParen,
61 #[error("malformed bounded repetition: {0}")]
62 MalformedBound(String),
63}
64
65pub fn parse_rpq(expr: &str) -> Result<RegularPathExpr, RPQParseError> {
66 let tokens = tokenize(expr);
67 let (result, pos) = parse_alternation(&tokens, 0)?;
68 if pos != tokens.len() {
69 return Err(RPQParseError::Unexpected {
70 position: pos,
71 token: tokens[pos].clone(),
72 });
73 }
74 Ok(result)
75}
76
77fn tokenize(expr: &str) -> Vec<String> {
78 let mut tokens = Vec::new();
79 let bytes = expr.as_bytes();
80 let mut i = 0;
81 while i < bytes.len() {
82 let ch = bytes[i] as char;
83 if ch.is_ascii_whitespace() {
84 i += 1;
85 continue;
86 }
87 if matches!(ch, '(' | ')' | '/' | '|' | '*' | '{' | '}' | ',') {
88 tokens.push(ch.to_string());
89 i += 1;
90 } else {
91 let start = i;
92 while i < bytes.len() {
93 let c = bytes[i] as char;
94 if c.is_ascii_whitespace()
95 || matches!(c, '(' | ')' | '/' | '|' | '*' | '{' | '}' | ',')
96 {
97 break;
98 }
99 i += 1;
100 }
101 tokens.push(expr[start..i].to_string());
102 }
103 }
104 tokens
105}
106
107fn parse_alternation(
108 tokens: &[String],
109 mut pos: usize,
110) -> Result<(RegularPathExpr, usize), RPQParseError> {
111 let (mut left, p) = parse_concat(tokens, pos)?;
112 pos = p;
113 while pos < tokens.len() && tokens[pos] == "|" {
114 pos += 1;
115 let (right, p) = parse_concat(tokens, pos)?;
116 pos = p;
117 left = RegularPathExpr::alt(left, right);
118 }
119 Ok((left, pos))
120}
121
122fn parse_concat(
123 tokens: &[String],
124 mut pos: usize,
125) -> Result<(RegularPathExpr, usize), RPQParseError> {
126 let (mut left, p) = parse_star(tokens, pos)?;
127 pos = p;
128 while pos < tokens.len() && tokens[pos] == "/" {
129 pos += 1;
130 let (right, p) = parse_star(tokens, pos)?;
131 pos = p;
132 left = RegularPathExpr::concat(left, right);
133 }
134 Ok((left, pos))
135}
136
137fn parse_star(
138 tokens: &[String],
139 mut pos: usize,
140) -> Result<(RegularPathExpr, usize), RPQParseError> {
141 let (mut expr, p) = parse_atom(tokens, pos)?;
142 pos = p;
143 while pos < tokens.len() && (tokens[pos] == "*" || tokens[pos] == "{") {
144 if tokens[pos] == "*" {
145 pos += 1;
146 expr = RegularPathExpr::star(expr);
147 } else {
148 pos += 1;
149 let min = tokens
150 .get(pos)
151 .ok_or_else(|| RPQParseError::MalformedBound("missing min".into()))?
152 .parse::<u32>()
153 .map_err(|e| RPQParseError::MalformedBound(format!("min: {e}")))?;
154 pos += 1;
155 if tokens.get(pos).map(String::as_str) != Some(",") {
156 return Err(RPQParseError::MalformedBound("expected ','".into()));
157 }
158 pos += 1;
159 let max = tokens
160 .get(pos)
161 .ok_or_else(|| RPQParseError::MalformedBound("missing max".into()))?
162 .parse::<u32>()
163 .map_err(|e| RPQParseError::MalformedBound(format!("max: {e}")))?;
164 if min > max {
165 return Err(RPQParseError::MalformedBound(format!(
166 "min {min} exceeds max {max}"
167 )));
168 }
169 pos += 1;
170 if tokens.get(pos).map(String::as_str) != Some("}") {
171 return Err(RPQParseError::MalformedBound("expected '}'".into()));
172 }
173 pos += 1;
174 expr = RegularPathExpr::bounded(expr, min, max);
175 }
176 }
177 Ok((expr, pos))
178}
179
180fn parse_atom(
181 tokens: &[String],
182 mut pos: usize,
183) -> Result<(RegularPathExpr, usize), RPQParseError> {
184 let token = tokens.get(pos).ok_or(RPQParseError::Eof)?;
185 if token == "(" {
186 pos += 1;
187 let (inner, p) = parse_alternation(tokens, pos)?;
188 pos = p;
189 if tokens.get(pos).map(String::as_str) != Some(")") {
190 return Err(RPQParseError::MissingParen);
191 }
192 pos += 1;
193 Ok((inner, pos))
194 } else if matches!(token.as_str(), ")" | "/" | "|" | "*" | "{" | "}" | ",") {
195 Err(RPQParseError::Unexpected {
196 position: pos,
197 token: token.clone(),
198 })
199 } else {
200 pos += 1;
201 Ok((RegularPathExpr::label(token.clone()), pos))
202 }
203}
204
205#[cfg(test)]
206mod tests;