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