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::types::{ProtoModifiedValueType, ProtoType, ProtoTypeRegistry, ProtoValueType};
159
160    #[test]
161    fn test_exists_one_syntax() {
162        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, crate::extern_paths::ExternPaths::new(crate::Mode::Prost));
163        let compiler = Compiler::new(&registry);
164        insta::assert_debug_snapshot!(ExistsOne.compile(CompilerCtx::new(compiler.child(), None, &[])), @r#"
165        Err(
166            InvalidSyntax {
167                message: "missing this",
168                syntax: "<this>.existsOne(<ident>, <expr>)",
169            },
170        )
171        "#);
172
173        insta::assert_debug_snapshot!(ExistsOne.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[])), @r#"
174        Err(
175            InvalidSyntax {
176                message: "invalid number of args",
177                syntax: "<this>.existsOne(<ident>, <expr>)",
178            },
179        )
180        "#);
181
182        insta::assert_debug_snapshot!(ExistsOne.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[
183            cel_parser::parse("x").unwrap(),
184            cel_parser::parse("dyn(x >= 1)").unwrap(),
185        ])), @r#"
186        Err(
187            TypeConversion {
188                ty: CelValue,
189                message: "String(Borrowed(\"hi\")) cannot be iterated over",
190            },
191        )
192        "#);
193
194        insta::assert_debug_snapshot!(ExistsOne.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::runtime(CelType::Proto(ProtoType::Value(ProtoValueType::Bool)), parse_quote!(input))), &[
195            cel_parser::parse("x").unwrap(),
196            cel_parser::parse("dyn(x >= 1)").unwrap(),
197        ])), @r#"
198        Err(
199            TypeConversion {
200                ty: Proto(
201                    Value(
202                        Bool,
203                    ),
204                ),
205                message: "type cannot be iterated over",
206            },
207        )
208        "#);
209
210        insta::assert_debug_snapshot!(ExistsOne.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::List([
211            CelValueConv::conv("value")
212        ].into_iter().collect()))), &[
213            cel_parser::parse("x").unwrap(),
214            cel_parser::parse("x == 'value'").unwrap(),
215        ])), @r"
216        Ok(
217            Constant(
218                ConstantCompiledExpr {
219                    value: Bool(
220                        true,
221                    ),
222                },
223            ),
224        )
225        ");
226
227        let input = CompiledExpr::constant(CelValue::Map(
228            [(CelValueConv::conv("value"), CelValueConv::conv(1))].into_iter().collect(),
229        ));
230        let mut ctx = compiler.child();
231        ctx.add_variable("input", input.clone());
232
233        insta::assert_debug_snapshot!(ExistsOne.compile(CompilerCtx::new(ctx, Some(input), &[
234            cel_parser::parse("x").unwrap(),
235            cel_parser::parse("input[x] > 0").unwrap(),
236        ])), @r"
237        Ok(
238            Constant(
239                ConstantCompiledExpr {
240                    value: Bool(
241                        true,
242                    ),
243                },
244            ),
245        )
246        ");
247    }
248
249    #[test]
250    #[cfg(not(valgrind))]
251    fn test_exists_one_runtime_map() {
252        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, crate::extern_paths::ExternPaths::new(crate::Mode::Prost));
253        let compiler = Compiler::new(&registry);
254
255        let string_value = CompiledExpr::runtime(
256            CelType::Proto(ProtoType::Modified(ProtoModifiedValueType::Map(
257                ProtoValueType::String,
258                ProtoValueType::Bool,
259            ))),
260            parse_quote!(input),
261        );
262
263        let output = ExistsOne
264            .compile(CompilerCtx::new(
265                compiler.child(),
266                Some(string_value),
267                &[cel_parser::parse("x").unwrap(), cel_parser::parse("x == 'value'").unwrap()],
268            ))
269            .unwrap();
270
271        insta::assert_snapshot!(postcompile::compile_str!(
272            postcompile::config! {
273                test: true,
274                dependencies: vec![
275                    postcompile::Dependency::version("tinc", "*"),
276                ],
277            },
278            quote! {
279                fn exists_one(input: &std::collections::HashMap<String, bool>) -> Result<bool, ::tinc::__private::cel::CelError<'_>> {
280                    Ok(#output)
281                }
282
283                #[test]
284                fn test_exists_one() {
285                    assert_eq!(exists_one(&{
286                        let mut map = std::collections::HashMap::new();
287                        map.insert("value".to_string(), true);
288                        map
289                    }).unwrap(), true);
290                    assert_eq!(exists_one(&{
291                        let mut map = std::collections::HashMap::new();
292                        map.insert("not_value".to_string(), true);
293                        map
294                    }).unwrap(), false);
295                    assert_eq!(exists_one(&{
296                        let mut map = std::collections::HashMap::new();
297                        map.insert("xd".to_string(), true);
298                        map.insert("value".to_string(), true);
299                        map
300                    }).unwrap(), true);
301                }
302            },
303        ));
304    }
305
306    #[test]
307    #[cfg(not(valgrind))]
308    fn test_exists_one_runtime_repeated() {
309        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, crate::extern_paths::ExternPaths::new(crate::Mode::Prost));
310        let compiler = Compiler::new(&registry);
311
312        let string_value = CompiledExpr::runtime(
313            CelType::Proto(ProtoType::Modified(ProtoModifiedValueType::Repeated(ProtoValueType::String))),
314            parse_quote!(input),
315        );
316
317        let output = ExistsOne
318            .compile(CompilerCtx::new(
319                compiler.child(),
320                Some(string_value),
321                &[cel_parser::parse("x").unwrap(), cel_parser::parse("x == 'value'").unwrap()],
322            ))
323            .unwrap();
324
325        insta::assert_snapshot!(postcompile::compile_str!(
326            postcompile::config! {
327                test: true,
328                dependencies: vec![
329                    postcompile::Dependency::version("tinc", "*"),
330                ],
331            },
332            quote! {
333                fn contains(input: &Vec<String>) -> Result<bool, ::tinc::__private::cel::CelError<'_>> {
334                    Ok(#output)
335                }
336
337                #[test]
338                fn test_contains() {
339                    assert_eq!(contains(&vec!["value".into()]).unwrap(), true);
340                    assert_eq!(contains(&vec!["not_value".into()]).unwrap(), false);
341                    assert_eq!(contains(&vec!["xd".into(), "value".into()]).unwrap(), true);
342                    assert_eq!(contains(&vec!["xd".into(), "value".into(), "value".into()]).unwrap(), false);
343                }
344            },
345        ));
346    }
347
348    #[test]
349    #[cfg(not(valgrind))]
350    fn test_exists_one_runtime_cel_value() {
351        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, crate::extern_paths::ExternPaths::new(crate::Mode::Prost));
352        let compiler = Compiler::new(&registry);
353
354        let string_value = CompiledExpr::runtime(CelType::CelValue, parse_quote!(input));
355
356        let output = ExistsOne
357            .compile(CompilerCtx::new(
358                compiler.child(),
359                Some(string_value),
360                &[cel_parser::parse("x").unwrap(), cel_parser::parse("x == 'value'").unwrap()],
361            ))
362            .unwrap();
363
364        insta::assert_snapshot!(postcompile::compile_str!(
365            postcompile::config! {
366                test: true,
367                dependencies: vec![
368                    postcompile::Dependency::version("tinc", "*"),
369                ],
370            },
371            quote! {
372                fn exists_one<'a>(input: &'a ::tinc::__private::cel::CelValue<'a>) -> Result<bool, ::tinc::__private::cel::CelError<'a>> {
373                    Ok(#output)
374                }
375
376                #[test]
377                fn test_exists_one() {
378                    assert_eq!(exists_one(&tinc::__private::cel::CelValue::List([
379                        tinc::__private::cel::CelValueConv::conv("value"),
380                    ].into_iter().collect())).unwrap(), true);
381                    assert_eq!(exists_one(&tinc::__private::cel::CelValue::List([
382                        tinc::__private::cel::CelValueConv::conv("not_value"),
383                    ].into_iter().collect())).unwrap(), false);
384                    assert_eq!(exists_one(&tinc::__private::cel::CelValue::List([
385                        tinc::__private::cel::CelValueConv::conv("xd"),
386                        tinc::__private::cel::CelValueConv::conv("value"),
387                    ].into_iter().collect())).unwrap(), true);
388                    assert_eq!(exists_one(&tinc::__private::cel::CelValue::List([
389                        tinc::__private::cel::CelValueConv::conv("xd"),
390                        tinc::__private::cel::CelValueConv::conv("value"),
391                        tinc::__private::cel::CelValueConv::conv("value"),
392                    ].into_iter().collect())).unwrap(), false);
393                }
394            },
395        ));
396    }
397
398    #[test]
399    #[cfg(not(valgrind))]
400    fn test_exists_one_const_requires_runtime() {
401        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, crate::extern_paths::ExternPaths::new(crate::Mode::Prost));
402        let compiler = Compiler::new(&registry);
403
404        let list_value = CompiledExpr::constant(CelValue::List(
405            [CelValueConv::conv(5), CelValueConv::conv(0), CelValueConv::conv(1)]
406                .into_iter()
407                .collect(),
408        ));
409
410        let output = ExistsOne
411            .compile(CompilerCtx::new(
412                compiler.child(),
413                Some(list_value),
414                &[cel_parser::parse("x").unwrap(), cel_parser::parse("dyn(x >= 1)").unwrap()],
415            ))
416            .unwrap();
417
418        insta::assert_snapshot!(postcompile::compile_str!(
419            postcompile::config! {
420                test: true,
421                dependencies: vec![
422                    postcompile::Dependency::version("tinc", "*"),
423                ],
424            },
425            quote! {
426                fn exists_one() -> Result<bool, ::tinc::__private::cel::CelError<'static>> {
427                    Ok(#output)
428                }
429
430                #[test]
431                fn test_filter() {
432                    assert_eq!(exists_one().unwrap(), false);
433                }
434            },
435        ));
436    }
437}