oxigdal_services/ogc_features/
cql.rs1use super::error::FeaturesError;
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum CqlValue {
8 String(String),
10 Number(f64),
12 Bool(bool),
14}
15
16#[derive(Debug, Clone, PartialEq)]
18pub enum CqlExpr {
19 Eq {
21 property: String,
23 value: CqlValue,
25 },
26 Lt {
28 property: String,
30 value: f64,
32 },
33 Lte {
35 property: String,
37 value: f64,
39 },
40 Gt {
42 property: String,
44 value: f64,
46 },
47 Gte {
49 property: String,
51 value: f64,
53 },
54 Like {
56 property: String,
58 pattern: String,
60 },
61 Between {
63 property: String,
65 low: f64,
67 high: f64,
69 },
70 And(Box<CqlExpr>, Box<CqlExpr>),
72 Or(Box<CqlExpr>, Box<CqlExpr>),
74 Not(Box<CqlExpr>),
76}
77
78pub struct CqlParser;
80
81impl CqlParser {
82 pub fn parse(input: &str) -> Result<CqlExpr, FeaturesError> {
92 let trimmed = input.trim();
93 Self::parse_or(trimmed)
94 }
95
96 fn parse_or(input: &str) -> Result<CqlExpr, FeaturesError> {
99 if let Some(idx) = Self::find_keyword_boundary(input, " OR ") {
101 let left = Self::parse_and(&input[..idx])?;
102 let right = Self::parse_or(&input[idx + 4..])?;
103 return Ok(CqlExpr::Or(Box::new(left), Box::new(right)));
104 }
105 Self::parse_and(input)
106 }
107
108 fn parse_and(input: &str) -> Result<CqlExpr, FeaturesError> {
109 if let Some(idx) = Self::find_and_not_between(input) {
111 let left = Self::parse_not(&input[..idx])?;
112 let right = Self::parse_and(&input[idx + 5..])?;
113 return Ok(CqlExpr::And(Box::new(left), Box::new(right)));
114 }
115 Self::parse_not(input)
116 }
117
118 fn parse_not(input: &str) -> Result<CqlExpr, FeaturesError> {
119 let s = input.trim();
120 let upper = s.to_ascii_uppercase();
121 if upper.starts_with("NOT ") {
122 let inner = s[4..].trim();
123 let inner = Self::strip_parens(inner);
125 return Ok(CqlExpr::Not(Box::new(Self::parse_or(inner)?)));
126 }
127 if s.starts_with('(') && s.ends_with(')') {
129 let inner = &s[1..s.len() - 1];
130 return Self::parse_or(inner.trim());
131 }
132 Self::parse_atom(s)
133 }
134
135 fn parse_atom(input: &str) -> Result<CqlExpr, FeaturesError> {
136 let s = input.trim();
137 let upper = s.to_ascii_uppercase();
138
139 if let Some(between_idx) = upper.find(" BETWEEN ") {
141 let property = s[..between_idx].trim().to_string();
142 let rest = &s[between_idx + 9..];
143 let upper_rest = rest.to_ascii_uppercase();
144 if let Some(and_idx) = upper_rest.find(" AND ") {
145 let low: f64 = rest[..and_idx].trim().parse().map_err(|_| {
146 FeaturesError::CqlParseError(format!(
147 "BETWEEN low bound not numeric: {}",
148 &rest[..and_idx]
149 ))
150 })?;
151 let high: f64 = rest[and_idx + 5..].trim().parse().map_err(|_| {
152 FeaturesError::CqlParseError(format!(
153 "BETWEEN high bound not numeric: {}",
154 &rest[and_idx + 5..]
155 ))
156 })?;
157 return Ok(CqlExpr::Between {
158 property,
159 low,
160 high,
161 });
162 }
163 }
164
165 if let Some(like_idx) = upper.find(" LIKE ") {
167 let property = s[..like_idx].trim().to_string();
168 let pattern_raw = s[like_idx + 6..].trim();
169 let pattern = Self::unquote(pattern_raw)?;
170 return Ok(CqlExpr::Like { property, pattern });
171 }
172
173 for (op_str, builder) in &[
175 (
176 ">=",
177 Self::build_gte as fn(&str, &str) -> Result<CqlExpr, FeaturesError>,
178 ),
179 ("<=", Self::build_lte),
180 ("!=", Self::build_neq_placeholder),
181 (">", Self::build_gt),
182 ("<", Self::build_lt),
183 ("=", Self::build_eq),
184 ] {
185 if let Some(op_idx) = s.find(op_str) {
186 let property = s[..op_idx].trim().to_string();
187 let value_str = s[op_idx + op_str.len()..].trim();
188 return builder(&property, value_str);
189 }
190 }
191
192 Err(FeaturesError::CqlParseError(format!(
193 "Cannot parse atom: {s}"
194 )))
195 }
196
197 fn build_eq(property: &str, value_str: &str) -> Result<CqlExpr, FeaturesError> {
200 let value = Self::parse_value(value_str)?;
201 Ok(CqlExpr::Eq {
202 property: property.to_string(),
203 value,
204 })
205 }
206
207 fn build_lt(property: &str, value_str: &str) -> Result<CqlExpr, FeaturesError> {
208 let v: f64 = value_str.parse().map_err(|_| {
209 FeaturesError::CqlParseError(format!("Expected number after '<': {value_str}"))
210 })?;
211 Ok(CqlExpr::Lt {
212 property: property.to_string(),
213 value: v,
214 })
215 }
216
217 fn build_lte(property: &str, value_str: &str) -> Result<CqlExpr, FeaturesError> {
218 let v: f64 = value_str.parse().map_err(|_| {
219 FeaturesError::CqlParseError(format!("Expected number after '<=': {value_str}"))
220 })?;
221 Ok(CqlExpr::Lte {
222 property: property.to_string(),
223 value: v,
224 })
225 }
226
227 fn build_gt(property: &str, value_str: &str) -> Result<CqlExpr, FeaturesError> {
228 let v: f64 = value_str.parse().map_err(|_| {
229 FeaturesError::CqlParseError(format!("Expected number after '>': {value_str}"))
230 })?;
231 Ok(CqlExpr::Gt {
232 property: property.to_string(),
233 value: v,
234 })
235 }
236
237 fn build_gte(property: &str, value_str: &str) -> Result<CqlExpr, FeaturesError> {
238 let v: f64 = value_str.parse().map_err(|_| {
239 FeaturesError::CqlParseError(format!("Expected number after '>=': {value_str}"))
240 })?;
241 Ok(CqlExpr::Gte {
242 property: property.to_string(),
243 value: v,
244 })
245 }
246
247 fn build_neq_placeholder(_property: &str, _value_str: &str) -> Result<CqlExpr, FeaturesError> {
248 Err(FeaturesError::CqlParseError(
249 "!= operator is not yet supported".to_string(),
250 ))
251 }
252
253 fn parse_value(s: &str) -> Result<CqlValue, FeaturesError> {
256 let s = s.trim();
257 if (s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')) {
259 return Ok(CqlValue::String(Self::unquote(s)?));
260 }
261 match s.to_ascii_uppercase().as_str() {
263 "TRUE" => return Ok(CqlValue::Bool(true)),
264 "FALSE" => return Ok(CqlValue::Bool(false)),
265 _ => {}
266 }
267 if let Ok(n) = s.parse::<f64>() {
269 return Ok(CqlValue::Number(n));
270 }
271 Ok(CqlValue::String(s.to_string()))
273 }
274
275 fn unquote(s: &str) -> Result<String, FeaturesError> {
276 let s = s.trim();
277 if (s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')) {
278 Ok(s[1..s.len() - 1].to_string())
279 } else {
280 Ok(s.to_string())
281 }
282 }
283
284 fn strip_parens(s: &str) -> &str {
285 let s = s.trim();
286 if s.starts_with('(') && s.ends_with(')') {
287 &s[1..s.len() - 1]
288 } else {
289 s
290 }
291 }
292
293 fn find_keyword_boundary(input: &str, keyword: &str) -> Option<usize> {
296 let upper = input.to_ascii_uppercase();
297 let kw_upper = keyword.to_ascii_uppercase();
298 let mut depth = 0usize;
299 let bytes = input.as_bytes();
300 let kw_len = kw_upper.len();
301 let kw_bytes = kw_upper.as_bytes();
302
303 let mut i = 0;
304 while i + kw_len <= bytes.len() {
305 match bytes[i] {
306 b'(' => {
307 depth += 1;
308 i += 1;
309 }
310 b')' => {
311 depth = depth.saturating_sub(1);
312 i += 1;
313 }
314 b'\'' | b'"' => {
315 let quote = bytes[i];
317 i += 1;
318 while i < bytes.len() && bytes[i] != quote {
319 i += 1;
320 }
321 i += 1; }
323 _ => {
324 if depth == 0 && upper.as_bytes()[i..].starts_with(kw_bytes) {
325 return Some(i);
326 }
327 i += 1;
328 }
329 }
330 }
331 None
332 }
333
334 fn find_and_not_between(input: &str) -> Option<usize> {
336 let upper = input.to_ascii_uppercase();
337 let mut search_start = 0;
338
339 while let Some(rel) = upper[search_start..].find(" AND ") {
340 let abs = search_start + rel;
341 let prefix = &upper[..abs];
343 if Self::is_between_and(prefix) {
345 search_start = abs + 5;
346 continue;
347 }
348 return Some(abs);
349 }
350 None
351 }
352
353 fn is_between_and(prefix: &str) -> bool {
358 let p = prefix.to_ascii_uppercase();
361 let between_count = p.matches(" BETWEEN ").count();
362 let and_count = p.matches(" AND ").count();
364 between_count > and_count
365 }
366
367 pub fn evaluate(expr: &CqlExpr, properties: &serde_json::Value) -> bool {
375 match expr {
376 CqlExpr::Eq { property, value } => {
377 let prop = Self::get_prop(properties, property);
378 match (value, &prop) {
379 (CqlValue::String(s), serde_json::Value::String(ps)) => s == ps,
380 (CqlValue::Number(n), serde_json::Value::Number(pn)) => {
381 pn.as_f64().is_some_and(|v| (v - n).abs() < f64::EPSILON)
382 }
383 (CqlValue::Bool(b), serde_json::Value::Bool(pb)) => b == pb,
384 _ => false,
385 }
386 }
387
388 CqlExpr::Lt { property, value } => {
389 Self::numeric_prop(properties, property).is_some_and(|v| v < *value)
390 }
391 CqlExpr::Lte { property, value } => {
392 Self::numeric_prop(properties, property).is_some_and(|v| v <= *value)
393 }
394 CqlExpr::Gt { property, value } => {
395 Self::numeric_prop(properties, property).is_some_and(|v| v > *value)
396 }
397 CqlExpr::Gte { property, value } => {
398 Self::numeric_prop(properties, property).is_some_and(|v| v >= *value)
399 }
400
401 CqlExpr::Like { property, pattern } => {
402 if let serde_json::Value::String(s) = Self::get_prop(properties, property) {
403 Self::like_match(&s, pattern)
404 } else {
405 false
406 }
407 }
408
409 CqlExpr::Between {
410 property,
411 low,
412 high,
413 } => Self::numeric_prop(properties, property).is_some_and(|v| v >= *low && v <= *high),
414
415 CqlExpr::And(a, b) => Self::evaluate(a, properties) && Self::evaluate(b, properties),
416 CqlExpr::Or(a, b) => Self::evaluate(a, properties) || Self::evaluate(b, properties),
417 CqlExpr::Not(inner) => !Self::evaluate(inner, properties),
418 }
419 }
420
421 fn get_prop(properties: &serde_json::Value, key: &str) -> serde_json::Value {
422 match properties {
423 serde_json::Value::Object(map) => {
424 map.get(key).cloned().unwrap_or(serde_json::Value::Null)
425 }
426 _ => serde_json::Value::Null,
427 }
428 }
429
430 fn numeric_prop(properties: &serde_json::Value, key: &str) -> Option<f64> {
431 match Self::get_prop(properties, key) {
432 serde_json::Value::Number(n) => n.as_f64(),
433 _ => None,
434 }
435 }
436
437 fn like_match(value: &str, pattern: &str) -> bool {
439 Self::like_recursive(value.as_bytes(), pattern.as_bytes())
440 }
441
442 fn like_recursive(value: &[u8], pattern: &[u8]) -> bool {
443 if pattern.is_empty() {
444 return value.is_empty();
445 }
446 match pattern[0] {
447 b'%' => {
448 for i in 0..=value.len() {
450 if Self::like_recursive(&value[i..], &pattern[1..]) {
451 return true;
452 }
453 }
454 false
455 }
456 b'_' => {
457 if value.is_empty() {
459 false
460 } else {
461 Self::like_recursive(&value[1..], &pattern[1..])
462 }
463 }
464 ch => {
465 if value.is_empty() || value[0] != ch {
466 false
467 } else {
468 Self::like_recursive(&value[1..], &pattern[1..])
469 }
470 }
471 }
472 }
473}