Skip to main content

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