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
29impl 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(®istry);
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(®istry);
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(®istry);
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(®istry);
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(®istry);
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}