ogc_cql2/
function.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4
5//! Expressions evaluation context.
6//!
7
8use crate::{Context, QString};
9use core::fmt;
10use geos::{Geom, Geometry};
11use jiff::{Zoned, tz::TimeZone};
12use std::any::Any;
13
14/// Externally visible data type variants for arguments and result types used
15/// and referenced by user-defined and registered functions invoked in filter
16/// expressions.
17#[derive(Debug)]
18pub enum ExtDataType {
19    /// A Unicode UTF-8 string.
20    Str,
21    /// A numeric value including integers and floating points.
22    Num,
23    /// A boolean value.
24    Bool,
25    /// An _Instant_ with a granularity of a second or smaller. Timestamps are
26    /// always in the time zone UTC ("Z").
27    Timestamp,
28    /// An _Instant_ with a granularity of a day. Dates are local without an
29    /// associated time zone.
30    Date,
31    /// A spatial (geometry) value.
32    Geom,
33}
34
35/// Type alias for a generic Function that may be invoked in the process of
36/// evaluating a CQL2 expression.
37type GenericFn = Box<dyn Fn(Vec<Box<dyn Any>>) -> Option<Box<dyn Any>> + Send + Sync + 'static>;
38
39/// A struct that holds metadata about a Function.
40pub struct FnInfo {
41    pub(crate) closure: GenericFn,
42    pub(crate) arg_types: Vec<ExtDataType>,
43    pub(crate) result_type: ExtDataType,
44}
45
46impl fmt::Debug for FnInfo {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("FnInfo")
49            // .field("z_fn", &self.z_fn)
50            .field("arg_types", &self.arg_types)
51            .field("return_type", &self.result_type)
52            .finish()
53    }
54}
55
56// FIXME (rsn) 20250820 - rewrite w/ a macro...
57pub(crate) fn add_builtins(ctx: &mut Context) {
58    // numeric functions as closures...
59    let abs = |x: f64| x.abs();
60    ctx.register(
61        "abs",
62        vec![ExtDataType::Num],
63        ExtDataType::Num,
64        move |args| {
65            let x = args.first()?.downcast_ref::<f64>()?;
66            Some(Box::new(abs(*x)))
67        },
68    );
69
70    let acos = |x: f64| x.acos();
71    ctx.register(
72        "acos",
73        vec![ExtDataType::Num],
74        ExtDataType::Num,
75        move |args| {
76            let x = args.first()?.downcast_ref::<f64>()?;
77            Some(Box::new(acos(*x)))
78        },
79    );
80
81    let asin = |x: f64| x.asin();
82    ctx.register(
83        "asin",
84        vec![ExtDataType::Num],
85        ExtDataType::Num,
86        move |args| {
87            let x = args.first()?.downcast_ref::<f64>()?;
88            Some(Box::new(asin(*x)))
89        },
90    );
91
92    let atan = |x: f64| x.atan();
93    ctx.register(
94        "atan",
95        vec![ExtDataType::Num],
96        ExtDataType::Num,
97        move |args| {
98            let x = args.first()?.downcast_ref::<f64>()?;
99            Some(Box::new(atan(*x)))
100        },
101    );
102
103    let cbrt = |x: f64| x.cbrt();
104    ctx.register(
105        "cbrt",
106        vec![ExtDataType::Num],
107        ExtDataType::Num,
108        move |args| {
109            let x = args.first()?.downcast_ref::<f64>()?;
110            Some(Box::new(cbrt(*x)))
111        },
112    );
113
114    let ceil = |x: f64| x.ceil();
115    ctx.register(
116        "ceil",
117        vec![ExtDataType::Num],
118        ExtDataType::Num,
119        move |args| {
120            let x = args.first()?.downcast_ref::<f64>()?;
121            Some(Box::new(ceil(*x)))
122        },
123    );
124
125    let cos = |x: f64| x.cos();
126    ctx.register(
127        "cos",
128        vec![ExtDataType::Num],
129        ExtDataType::Num,
130        move |args| {
131            let x = args.first()?.downcast_ref::<f64>()?;
132            Some(Box::new(cos(*x)))
133        },
134    );
135
136    let floor = |x: f64| x.floor();
137    ctx.register(
138        "floor",
139        vec![ExtDataType::Num],
140        ExtDataType::Num,
141        move |args| {
142            let x = args.first()?.downcast_ref::<f64>()?;
143            Some(Box::new(floor(*x)))
144        },
145    );
146
147    let ln = |x: f64| x.ln();
148    ctx.register(
149        "ln",
150        vec![ExtDataType::Num],
151        ExtDataType::Num,
152        move |args| {
153            let x = args.first()?.downcast_ref::<f64>()?;
154            Some(Box::new(ln(*x)))
155        },
156    );
157
158    let sin = |x: f64| x.sin();
159    ctx.register(
160        "sin",
161        vec![ExtDataType::Num],
162        ExtDataType::Num,
163        move |args| {
164            let x = args.first()?.downcast_ref::<f64>()?;
165            Some(Box::new(sin(*x)))
166        },
167    );
168
169    let sqrt = |x: f64| x.sqrt();
170    ctx.register(
171        "sqrt",
172        vec![ExtDataType::Num],
173        ExtDataType::Num,
174        move |args| {
175            let x = args.first()?.downcast_ref::<f64>()?;
176            Some(Box::new(sqrt(*x)))
177        },
178    );
179
180    let tan = |x: f64| x.tan();
181    ctx.register(
182        "tan",
183        vec![ExtDataType::Num],
184        ExtDataType::Num,
185        move |args| {
186            let x = args.first()?.downcast_ref::<f64>()?;
187            Some(Box::new(tan(*x)))
188        },
189    );
190
191    let max = |x: f64, y: f64| x.max(y);
192    ctx.register(
193        "max",
194        vec![ExtDataType::Num, ExtDataType::Num],
195        ExtDataType::Num,
196        move |args| {
197            let x = args.first()?.downcast_ref::<f64>()?;
198            let y = args.get(1)?.downcast_ref::<f64>()?;
199            Some(Box::new(max(*x, *y)))
200        },
201    );
202
203    let avg = |x: f64, y: f64| x.midpoint(y);
204    ctx.register(
205        "avg",
206        vec![ExtDataType::Num, ExtDataType::Num],
207        ExtDataType::Num,
208        move |args| {
209            let x = args.first()?.downcast_ref::<f64>()?;
210            let y = args.get(1)?.downcast_ref::<f64>()?;
211            Some(Box::new(avg(*x, *y)))
212        },
213    );
214
215    let min = |x: f64, y: f64| x.min(y);
216    ctx.register(
217        "min",
218        vec![ExtDataType::Num, ExtDataType::Num],
219        ExtDataType::Num,
220        move |args| {
221            let x = args.first()?.downcast_ref::<f64>()?;
222            let y = args.get(1)?.downcast_ref::<f64>()?;
223            Some(Box::new(min(*x, *y)))
224        },
225    );
226
227    // string builtins...
228    let trim = |x: &QString| x.as_str().trim().to_owned();
229    ctx.register(
230        "trim",
231        vec![ExtDataType::Str],
232        ExtDataType::Str,
233        move |args| {
234            let x = args.first()?.downcast_ref::<QString>()?;
235            Some(Box::new(trim(x)))
236        },
237    );
238
239    let len = |x: &QString| x.as_str().len();
240    ctx.register(
241        "len",
242        vec![ExtDataType::Str],
243        ExtDataType::Num,
244        move |args| {
245            let x = args.first()?.downcast_ref::<QString>()?;
246            Some(Box::new(len(x)))
247        },
248    );
249
250    let concat = |x: &QString, y: &QString| format!("{}{}", x.as_str(), y.as_str());
251    ctx.register(
252        "concat",
253        vec![ExtDataType::Str, ExtDataType::Str],
254        ExtDataType::Str,
255        move |args| {
256            let x = args.first()?.downcast_ref::<QString>()?;
257            let y = args.get(1)?.downcast_ref::<QString>()?;
258            Some(Box::new(concat(x, y)))
259        },
260    );
261
262    let starts_with = |x: &str, y: &str| x.starts_with(y);
263    ctx.register(
264        "starts_with",
265        vec![ExtDataType::Str, ExtDataType::Str],
266        ExtDataType::Bool,
267        move |args| {
268            let x = args.first()?.downcast_ref::<QString>()?.as_str();
269            let y = args.get(1)?.downcast_ref::<QString>()?.as_str();
270            Some(Box::new(starts_with(x, y)))
271        },
272    );
273
274    let ends_with = |x: &str, y: &str| x.ends_with(y);
275    ctx.register(
276        "ends_with",
277        vec![ExtDataType::Str, ExtDataType::Str],
278        ExtDataType::Bool,
279        move |args| {
280            let x = args.first()?.downcast_ref::<QString>()?.as_str();
281            let y = args.get(1)?.downcast_ref::<QString>()?.as_str();
282            Some(Box::new(ends_with(x, y)))
283        },
284    );
285
286    // temporal builtins...
287    let now = || Zoned::now().with_time_zone(TimeZone::UTC);
288    ctx.register("now", vec![], ExtDataType::Timestamp, move |_| {
289        Some(Box::new(now()))
290    });
291
292    let today = || {
293        let noon = Zoned::now()
294            .with()
295            .hour(12)
296            .minute(0)
297            .second(0)
298            .subsec_nanosecond(0)
299            .build()
300            .expect("Failed finding today's date");
301        noon.with_time_zone(TimeZone::UTC)
302    };
303    ctx.register("today", vec![], ExtDataType::Timestamp, move |_| {
304        Some(Box::new(today()))
305    });
306
307    // spatial builtins...
308    let boundary = |x: &Geometry| x.boundary().expect("Failed finding boundary");
309    ctx.register(
310        "boundary",
311        vec![ExtDataType::Geom],
312        ExtDataType::Geom,
313        move |args| {
314            let x = args.first()?.downcast_ref::<Geometry>()?;
315            Some(Box::new(boundary(x)))
316        },
317    );
318
319    let buffer = |x: &Geometry, y: &f64| x.buffer(*y, 8).expect("Failed computing buffer");
320    ctx.register(
321        "buffer",
322        vec![ExtDataType::Geom, ExtDataType::Num],
323        ExtDataType::Geom,
324        move |args| {
325            let x = args.first()?.downcast_ref::<Geometry>()?;
326            let y = args.get(1)?.downcast_ref::<f64>()?;
327            Some(Box::new(buffer(x, y)))
328        },
329    );
330
331    let envelope = |x: &Geometry| x.envelope().expect("Failed finding envelope");
332    ctx.register(
333        "envelope",
334        vec![ExtDataType::Geom],
335        ExtDataType::Geom,
336        move |args| {
337            let x = args.first()?.downcast_ref::<Geometry>()?;
338            Some(Box::new(envelope(x)))
339        },
340    );
341
342    let centroid = |x: &Geometry| x.get_centroid().expect("Failed finding centroid");
343    ctx.register(
344        "centroid",
345        vec![ExtDataType::Geom],
346        ExtDataType::Geom,
347        move |args| {
348            let x = args.first()?.downcast_ref::<Geometry>()?;
349            Some(Box::new(centroid(x)))
350        },
351    );
352
353    let convex_hull = |x: &Geometry| x.convex_hull().expect("Failed finding convex hull");
354    ctx.register(
355        "convex_hull",
356        vec![ExtDataType::Geom],
357        ExtDataType::Geom,
358        move |args| {
359            let x = args.first()?.downcast_ref::<Geometry>()?;
360            Some(Box::new(convex_hull(x)))
361        },
362    );
363
364    let get_x = |x: &Geometry| x.get_x().expect("Failed isolating X");
365    ctx.register(
366        "get_x",
367        vec![ExtDataType::Geom],
368        ExtDataType::Num,
369        move |args| {
370            let x = args.first()?.downcast_ref::<Geometry>()?;
371            Some(Box::new(get_x(x)))
372        },
373    );
374
375    let get_y = |x: &Geometry| x.get_y().expect("Failed isolating Y");
376    ctx.register(
377        "get_y",
378        vec![ExtDataType::Geom],
379        ExtDataType::Num,
380        move |args| {
381            let x = args.first()?.downcast_ref::<Geometry>()?;
382            Some(Box::new(get_y(x)))
383        },
384    );
385
386    let get_z = |x: &Geometry| x.get_y().expect("Failed isolating Z");
387    ctx.register(
388        "get_z",
389        vec![ExtDataType::Geom],
390        ExtDataType::Num,
391        move |args| {
392            let x = args.first()?.downcast_ref::<Geometry>()?;
393            Some(Box::new(get_z(x)))
394        },
395    );
396
397    let wkt = |x: &Geometry| x.to_wkt().expect("Failed generating WKT");
398    ctx.register(
399        "wkt",
400        vec![ExtDataType::Geom],
401        ExtDataType::Str,
402        move |args| {
403            let x = args.first()?.downcast_ref::<Geometry>()?;
404            Some(Box::new(wkt(x)))
405        },
406    );
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use crate::prelude::*;
413    use std::error::Error;
414
415    #[test]
416    // #[tracing_test::traced_test]
417    fn test_unregistered() -> Result<(), Box<dyn Error>> {
418        let shared_ctx = Context::new().freeze();
419
420        let expr = Expression::try_from_text("sum(a, b)")?;
421        let mut eval = EvaluatorImpl::new(shared_ctx);
422        eval.setup(expr)?;
423
424        let feat = Resource::new();
425        let res = eval.evaluate(&feat)?;
426        // tracing::debug!("res = {res:?}");
427        assert!(matches!(res, Outcome::N));
428
429        eval.teardown()?;
430
431        Ok(())
432    }
433
434    #[test]
435    // #[tracing_test::traced_test]
436    fn test_literals() -> Result<(), Box<dyn Error>> {
437        let sum = |x: f64, y: f64| x + y;
438
439        let mut ctx = Context::new();
440        ctx.register(
441            "sum",
442            vec![ExtDataType::Num, ExtDataType::Num],
443            ExtDataType::Num,
444            move |args| {
445                let a1 = args.get(0)?.downcast_ref::<f64>()?;
446                let a2 = args.get(1)?.downcast_ref::<f64>()?;
447                Some(Box::new(sum(*a1, *a2))) // Call the user-defined closure
448            },
449        );
450        let shared_ctx = ctx.freeze();
451
452        let expr = Expression::try_from_text("3 = sum(1, 2)")?;
453        let mut eval = EvaluatorImpl::new(shared_ctx);
454        eval.setup(expr)?;
455
456        let feat = Resource::new();
457
458        let res = eval.evaluate(&feat)?;
459        // tracing::debug!("res = {res:?}");
460        assert!(matches!(res, Outcome::T));
461
462        eval.teardown()?;
463
464        Ok(())
465    }
466
467    #[test]
468    // #[tracing_test::traced_test]
469    fn test_queryables() -> Result<(), Box<dyn Error>> {
470        let sum = |x: f64, y: f64| x + y;
471
472        let mut ctx = Context::new();
473        ctx.register(
474            "sum",
475            vec![ExtDataType::Num, ExtDataType::Num],
476            ExtDataType::Num,
477            move |args| {
478                let a1 = args.get(0)?.downcast_ref::<f64>()?;
479                let a2 = args.get(1)?.downcast_ref::<f64>()?;
480                Some(Box::new(sum(*a1, *a2))) // Call the user-defined closure
481            },
482        );
483        let shared_ctx = ctx.freeze();
484
485        let expr = Expression::try_from_text("30 = sum(a, b)")?;
486        let mut eval = EvaluatorImpl::new(shared_ctx);
487        eval.setup(expr)?;
488
489        let feat = Resource::from([
490            ("fid".into(), Q::try_from(1)?),
491            ("a".into(), Q::try_from(10)?),
492            ("b".into(), Q::try_from(20.0)?),
493        ]);
494
495        let res = eval.evaluate(&feat)?;
496        // tracing::debug!("res = {res:?}");
497        assert!(matches!(res, Outcome::T));
498
499        eval.teardown()?;
500
501        Ok(())
502    }
503
504    #[test]
505    // #[tracing_test::traced_test]
506    fn test_wrong_data_type() -> Result<(), Box<dyn Error>> {
507        let sum = |x: f64, y: f64| x + y;
508
509        let mut ctx = Context::new();
510        ctx.register(
511            "sum",
512            vec![ExtDataType::Num, ExtDataType::Num],
513            ExtDataType::Num,
514            move |args| {
515                let a1 = args.get(0)?.downcast_ref::<f64>()?;
516                let a2 = args.get(1)?.downcast_ref::<f64>()?;
517                Some(Box::new(sum(*a1, *a2))) // Call the user-defined closure
518            },
519        );
520        let shared_ctx = ctx.freeze();
521
522        let expr = Expression::try_from_text("30 = sum(a, b)")?;
523        let mut eval = EvaluatorImpl::new(shared_ctx);
524        eval.setup(expr)?;
525
526        let feat = Resource::from([
527            ("fid".into(), Q::try_from(1)?),
528            ("a".into(), Q::try_from(10)?),
529            ("b".into(), Q::new_plain_str("20.0")),
530        ]);
531
532        let res = eval.evaluate(&feat);
533        // tracing::debug!("res = {res:?}");
534        assert!(res.is_err());
535
536        eval.teardown()?;
537
538        Ok(())
539    }
540
541    #[test]
542    // #[tracing_test::traced_test]
543    fn test_num_builtins() -> Result<(), Box<dyn Error>> {
544        let mut ctx = Context::new();
545        ctx.register_builtins();
546        let shared_ctx = ctx.freeze();
547
548        let expr = Expression::try_from_text("min(a, b) + max(a, b) = 2 * avg(a, b)")?;
549        let mut eval = EvaluatorImpl::new(shared_ctx);
550        eval.setup(expr)?;
551
552        let feat = Resource::from([
553            ("fid".into(), Q::try_from(1)?),
554            ("a".into(), Q::try_from(10)?),
555            ("b".into(), Q::try_from(20)?),
556        ]);
557
558        let res = eval.evaluate(&feat)?;
559        // tracing::debug!("res = {res:?}");
560        assert!(matches!(res, Outcome::T));
561
562        Ok(())
563    }
564
565    #[test]
566    // #[tracing_test::traced_test]
567    fn test_geom_builtins() -> Result<(), Box<dyn Error>> {
568        // IMPORTANT (rsn) 20250901 - if we rely on Context::new() we leave
569        // the context subject to the global configuration which may be using
570        // an implicit CRS code that's unexpected for the test.  specifically
571        // the pre-conditions of this test expect WGS-84 coordinates...
572        let mut ctx = Context::try_with_crs("epsg:4326")?;
573        ctx.register_builtins();
574        let shared_ctx = ctx.freeze();
575
576        let expr = Expression::try_from_text(
577            "wkt(centroid(envelope(MULTIPOINT(0 90, 90 0)))) = 'POINT (45 45)'",
578        )?;
579        let mut eval = EvaluatorImpl::new(shared_ctx);
580        eval.setup(expr)?;
581
582        let feat = Resource::new();
583
584        let res = eval.evaluate(&feat)?;
585        // tracing::debug!("res = {res:?}");
586        assert!(matches!(res, Outcome::T));
587
588        Ok(())
589    }
590
591    #[test]
592    #[tracing_test::traced_test]
593    fn test_str_builtins() -> Result<(), Box<dyn Error>> {
594        let mut ctx = Context::new();
595        ctx.register_builtins();
596        let shared_ctx = ctx.freeze();
597
598        let expr = Expression::try_from_text("starts_with(concat('foo', 'bar'), 'fo')")?;
599        let mut eval = EvaluatorImpl::new(shared_ctx);
600        eval.setup(expr)?;
601
602        let feat = Resource::new();
603
604        let res = eval.evaluate(&feat)?;
605        tracing::debug!("res = {res:?}");
606        assert!(matches!(res, Outcome::T));
607
608        Ok(())
609    }
610}