tinc_build/codegen/cel/functions/
exists.rs

1use proc_macro2::TokenStream;
2use quote::{ToTokens, quote};
3use syn::parse_quote;
4use tinc_cel::CelValue;
5
6use super::Function;
7use crate::codegen::cel::compiler::{CompileError, CompiledExpr, CompilerCtx, ConstantCompiledExpr, RuntimeCompiledExpr};
8use crate::codegen::cel::types::CelType;
9use crate::types::{ProtoModifiedValueType, ProtoType, ProtoValueType};
10
11#[derive(Debug, Clone, Default)]
12pub(crate) struct Exists;
13
14fn native_impl(iter: TokenStream, item_ident: syn::Ident, compare: impl ToTokens) -> syn::Expr {
15    parse_quote!({
16        let mut iter = (#iter).into_iter();
17        loop {
18            let Some(#item_ident) = iter.next() else {
19                break false;
20            };
21
22            if #compare {
23                break true;
24            }
25        }
26    })
27}
28
29// this.exists(<ident>, <expr>)
30impl Function for Exists {
31    fn name(&self) -> &'static str {
32        "exists"
33    }
34
35    fn syntax(&self) -> &'static str {
36        "<this>.exists(<ident>, <expr>)"
37    }
38
39    fn compile(&self, mut ctx: CompilerCtx) -> Result<CompiledExpr, CompileError> {
40        let Some(this) = ctx.this.take() else {
41            return Err(CompileError::syntax("missing this", self));
42        };
43
44        if ctx.args.len() != 2 {
45            return Err(CompileError::syntax("invalid number of args", self));
46        }
47
48        let cel_parser::Expression::Ident(variable) = &ctx.args[0] else {
49            return Err(CompileError::syntax("first argument must be an ident", self));
50        };
51
52        match this {
53            CompiledExpr::Runtime(RuntimeCompiledExpr { expr, ty }) => {
54                let mut child_ctx = ctx.child();
55
56                match &ty {
57                    CelType::CelValue => {
58                        child_ctx.add_variable(variable, CompiledExpr::runtime(CelType::CelValue, parse_quote!(item)));
59                    }
60                    CelType::Proto(ProtoType::Modified(
61                        ProtoModifiedValueType::Repeated(ty) | ProtoModifiedValueType::Map(ty, _),
62                    )) => {
63                        child_ctx.add_variable(
64                            variable,
65                            CompiledExpr::runtime(CelType::Proto(ProtoType::Value(ty.clone())), parse_quote!(item)),
66                        );
67                    }
68                    v => {
69                        return Err(CompileError::TypeConversion {
70                            ty: Box::new(v.clone()),
71                            message: "type cannot be iterated over".to_string(),
72                        });
73                    }
74                };
75
76                let arg = child_ctx.resolve(&ctx.args[1])?.into_bool(&child_ctx);
77
78                Ok(CompiledExpr::runtime(
79                    CelType::Proto(ProtoType::Value(ProtoValueType::Bool)),
80                    match &ty {
81                        CelType::CelValue => parse_quote! {
82                            ::tinc::__private::cel::CelValue::cel_exists(#expr, |item| {
83                                ::core::result::Result::Ok(
84                                    #arg
85                                )
86                            })?
87                        },
88                        CelType::Proto(ProtoType::Modified(ProtoModifiedValueType::Map(_, _))) => {
89                            native_impl(quote!((#expr).keys()), parse_quote!(item), arg)
90                        }
91                        CelType::Proto(ProtoType::Modified(ProtoModifiedValueType::Repeated(_))) => {
92                            native_impl(quote!((#expr).iter()), parse_quote!(item), arg)
93                        }
94                        _ => unreachable!(),
95                    },
96                ))
97            }
98            CompiledExpr::Constant(ConstantCompiledExpr {
99                value: value @ (CelValue::List(_) | CelValue::Map(_)),
100            }) => {
101                let compile_val = |value: CelValue<'static>| {
102                    let mut child_ctx = ctx.child();
103
104                    child_ctx.add_variable(variable, CompiledExpr::constant(value));
105
106                    child_ctx.resolve(&ctx.args[1]).map(|v| v.into_bool(&child_ctx))
107                };
108
109                let collected: Result<Vec<_>, _> = match value {
110                    CelValue::List(item) => item.iter().cloned().map(compile_val).collect(),
111                    CelValue::Map(item) => item.iter().map(|(key, _)| key).cloned().map(compile_val).collect(),
112                    _ => unreachable!(),
113                };
114
115                let collected = collected?;
116                if collected.iter().any(|c| matches!(c, CompiledExpr::Runtime(_))) {
117                    Ok(CompiledExpr::runtime(
118                        CelType::Proto(ProtoType::Value(ProtoValueType::Bool)),
119                        native_impl(quote!([#(#collected),*]), parse_quote!(item), quote!(item)),
120                    ))
121                } else {
122                    Ok(CompiledExpr::constant(CelValue::Bool(collected.into_iter().any(
123                        |c| match c {
124                            CompiledExpr::Constant(ConstantCompiledExpr { value }) => value.to_bool(),
125                            _ => unreachable!("all values must be constant"),
126                        },
127                    ))))
128                }
129            }
130            CompiledExpr::Constant(ConstantCompiledExpr { value }) => Err(CompileError::TypeConversion {
131                ty: Box::new(CelType::CelValue),
132                message: format!("{value:?} cannot be iterated over"),
133            }),
134        }
135    }
136}
137
138#[cfg(test)]
139#[cfg(feature = "prost")]
140#[cfg_attr(coverage_nightly, coverage(off))]
141mod tests {
142    use quote::quote;
143    use syn::parse_quote;
144    use tinc_cel::{CelValue, CelValueConv};
145
146    use crate::codegen::cel::compiler::{CompiledExpr, Compiler, CompilerCtx};
147    use crate::codegen::cel::functions::{Exists, Function};
148    use crate::codegen::cel::types::CelType;
149    use crate::extern_paths::ExternPaths;
150    use crate::path_set::PathSet;
151    use crate::types::{ProtoModifiedValueType, ProtoType, ProtoTypeRegistry, ProtoValueType};
152
153    #[test]
154    fn test_exists_syntax() {
155        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, ExternPaths::new(crate::Mode::Prost), PathSet::default());
156        let compiler = Compiler::new(&registry);
157        insta::assert_debug_snapshot!(Exists.compile(CompilerCtx::new(compiler.child(), None, &[])), @r#"
158        Err(
159            InvalidSyntax {
160                message: "missing this",
161                syntax: "<this>.exists(<ident>, <expr>)",
162            },
163        )
164        "#);
165
166        insta::assert_debug_snapshot!(Exists.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[])), @r#"
167        Err(
168            InvalidSyntax {
169                message: "invalid number of args",
170                syntax: "<this>.exists(<ident>, <expr>)",
171            },
172        )
173        "#);
174
175        insta::assert_debug_snapshot!(Exists.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[
176            cel_parser::parse("x").unwrap(),
177            cel_parser::parse("dyn(x >= 1)").unwrap(),
178        ])), @r#"
179        Err(
180            TypeConversion {
181                ty: CelValue,
182                message: "String(Borrowed(\"hi\")) cannot be iterated over",
183            },
184        )
185        "#);
186
187        insta::assert_debug_snapshot!(Exists.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::runtime(CelType::Proto(ProtoType::Value(ProtoValueType::Bool)), parse_quote!(input))), &[
188            cel_parser::parse("x").unwrap(),
189            cel_parser::parse("dyn(x >= 1)").unwrap(),
190        ])), @r#"
191        Err(
192            TypeConversion {
193                ty: Proto(
194                    Value(
195                        Bool,
196                    ),
197                ),
198                message: "type cannot be iterated over",
199            },
200        )
201        "#);
202
203        insta::assert_debug_snapshot!(Exists.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::List(Default::default()))), &[
204            cel_parser::parse("x").unwrap(),
205            cel_parser::parse("x == 'value'").unwrap(),
206        ])), @r"
207        Ok(
208            Constant(
209                ConstantCompiledExpr {
210                    value: Bool(
211                        false,
212                    ),
213                },
214            ),
215        )
216        ");
217    }
218
219    #[test]
220    #[cfg(not(valgrind))]
221    fn test_exists_runtime_map() {
222        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, ExternPaths::new(crate::Mode::Prost), PathSet::default());
223        let compiler = Compiler::new(&registry);
224
225        let string_value = CompiledExpr::runtime(
226            CelType::Proto(ProtoType::Modified(ProtoModifiedValueType::Map(
227                ProtoValueType::String,
228                ProtoValueType::Bool,
229            ))),
230            parse_quote!(input),
231        );
232
233        let output = Exists
234            .compile(CompilerCtx::new(
235                compiler.child(),
236                Some(string_value),
237                &[cel_parser::parse("x").unwrap(), cel_parser::parse("x == 'value'").unwrap()],
238            ))
239            .unwrap();
240
241        insta::assert_snapshot!(postcompile::compile_str!(
242            postcompile::config! {
243                test: true,
244                dependencies: vec![
245                    postcompile::Dependency::version("tinc", "*"),
246                ],
247            },
248            quote! {
249                fn exists(input: &std::collections::HashMap<String, bool>) -> Result<bool, ::tinc::__private::cel::CelError<'_>> {
250                    Ok(#output)
251                }
252
253                #[test]
254                fn test_contains() {
255                    assert_eq!(exists(&{
256                        let mut map = std::collections::HashMap::new();
257                        map.insert("value".to_string(), true);
258                        map
259                    }).unwrap(), true);
260                    assert_eq!(exists(&{
261                        let mut map = std::collections::HashMap::new();
262                        map.insert("not_value".to_string(), true);
263                        map
264                    }).unwrap(), false);
265                    assert_eq!(exists(&{
266                        let mut map = std::collections::HashMap::new();
267                        map.insert("xd".to_string(), true);
268                        map.insert("value".to_string(), true);
269                        map
270                    }).unwrap(), true);
271                }
272            },
273        ));
274    }
275
276    #[test]
277    #[cfg(not(valgrind))]
278    fn test_exists_runtime_repeated() {
279        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, ExternPaths::new(crate::Mode::Prost), PathSet::default());
280        let compiler = Compiler::new(&registry);
281
282        let string_value = CompiledExpr::runtime(
283            CelType::Proto(ProtoType::Modified(ProtoModifiedValueType::Repeated(ProtoValueType::String))),
284            parse_quote!(input),
285        );
286
287        let output = Exists
288            .compile(CompilerCtx::new(
289                compiler.child(),
290                Some(string_value),
291                &[cel_parser::parse("x").unwrap(), cel_parser::parse("x == 'value'").unwrap()],
292            ))
293            .unwrap();
294
295        insta::assert_snapshot!(postcompile::compile_str!(
296            postcompile::config! {
297                test: true,
298                dependencies: vec![
299                    postcompile::Dependency::version("tinc", "*"),
300                ],
301            },
302            quote! {
303                fn exists(input: &Vec<String>) -> Result<bool, ::tinc::__private::cel::CelError<'_>> {
304                    Ok(#output)
305                }
306
307                #[test]
308                fn test_exists() {
309                    assert_eq!(exists(&vec!["value".into()]).unwrap(), true);
310                    assert_eq!(exists(&vec!["not_value".into()]).unwrap(), false);
311                    assert_eq!(exists(&vec!["xd".into(), "value".into()]).unwrap(), true);
312                    assert_eq!(exists(&vec!["xd".into(), "value".into(), "value".into()]).unwrap(), true);
313                }
314            },
315        ));
316    }
317
318    #[test]
319    #[cfg(not(valgrind))]
320    fn test_exists_runtime_cel_value() {
321        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, ExternPaths::new(crate::Mode::Prost), PathSet::default());
322        let compiler = Compiler::new(&registry);
323
324        let string_value = CompiledExpr::runtime(CelType::CelValue, parse_quote!(input));
325
326        let output = Exists
327            .compile(CompilerCtx::new(
328                compiler.child(),
329                Some(string_value),
330                &[cel_parser::parse("x").unwrap(), cel_parser::parse("x == 'value'").unwrap()],
331            ))
332            .unwrap();
333
334        insta::assert_snapshot!(postcompile::compile_str!(
335            postcompile::config! {
336                test: true,
337                dependencies: vec![
338                    postcompile::Dependency::version("tinc", "*"),
339                ],
340            },
341            quote! {
342                fn exists<'a>(input: &'a ::tinc::__private::cel::CelValue<'a>) -> Result<bool, ::tinc::__private::cel::CelError<'a>> {
343                    Ok(#output)
344                }
345
346                #[test]
347                fn test_exists() {
348                    assert_eq!(exists(&tinc::__private::cel::CelValue::List([
349                        tinc::__private::cel::CelValueConv::conv("value"),
350                    ].into_iter().collect())).unwrap(), true);
351                    assert_eq!(exists(&tinc::__private::cel::CelValue::List([
352                        tinc::__private::cel::CelValueConv::conv("not_value"),
353                    ].into_iter().collect())).unwrap(), false);
354                    assert_eq!(exists(&tinc::__private::cel::CelValue::List([
355                        tinc::__private::cel::CelValueConv::conv("xd"),
356                        tinc::__private::cel::CelValueConv::conv("value"),
357                    ].into_iter().collect())).unwrap(), true);
358                    assert_eq!(exists(&tinc::__private::cel::CelValue::List([
359                        tinc::__private::cel::CelValueConv::conv("xd"),
360                        tinc::__private::cel::CelValueConv::conv("value"),
361                        tinc::__private::cel::CelValueConv::conv("value"),
362                    ].into_iter().collect())).unwrap(), true);
363                }
364            },
365        ));
366    }
367
368    #[test]
369    #[cfg(not(valgrind))]
370    fn test_exists_const_requires_runtime() {
371        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, ExternPaths::new(crate::Mode::Prost), PathSet::default());
372        let compiler = Compiler::new(&registry);
373
374        let list_value = CompiledExpr::constant(CelValue::List(
375            [CelValueConv::conv(5), CelValueConv::conv(0), CelValueConv::conv(1)]
376                .into_iter()
377                .collect(),
378        ));
379
380        let output = Exists
381            .compile(CompilerCtx::new(
382                compiler.child(),
383                Some(list_value),
384                &[cel_parser::parse("x").unwrap(), cel_parser::parse("dyn(x >= 1)").unwrap()],
385            ))
386            .unwrap();
387
388        insta::assert_snapshot!(postcompile::compile_str!(
389            postcompile::config! {
390                test: true,
391                dependencies: vec![
392                    postcompile::Dependency::version("tinc", "*"),
393                ],
394            },
395            quote! {
396                fn exists_one() -> Result<bool, ::tinc::__private::cel::CelError<'static>> {
397                    Ok(#output)
398                }
399
400                #[test]
401                fn test_filter() {
402                    assert_eq!(exists_one().unwrap(), true);
403                }
404            },
405        ));
406    }
407}