tinc_build/codegen/cel/functions/
matches.rs

1use syn::parse_quote;
2use tinc_cel::CelValue;
3
4use super::Function;
5use crate::codegen::cel::compiler::{CompileError, CompiledExpr, CompilerCtx, ConstantCompiledExpr};
6use crate::codegen::cel::types::CelType;
7use crate::types::{ProtoType, ProtoValueType};
8
9#[derive(Debug, Clone, Default)]
10pub(crate) struct Matches;
11
12// this.matches(arg) -> arg in this
13impl Function for Matches {
14    fn name(&self) -> &'static str {
15        "matches"
16    }
17
18    fn syntax(&self) -> &'static str {
19        "<this>.matches(<const regex>)"
20    }
21
22    fn compile(&self, ctx: CompilerCtx) -> Result<CompiledExpr, CompileError> {
23        let Some(this) = &ctx.this else {
24            return Err(CompileError::syntax("missing this", self));
25        };
26
27        if ctx.args.len() != 1 {
28            return Err(CompileError::syntax("takes exactly one argument", self));
29        }
30
31        let CompiledExpr::Constant(ConstantCompiledExpr {
32            value: CelValue::String(regex),
33        }) = ctx.resolve(&ctx.args[0])?.into_cel()?
34        else {
35            return Err(CompileError::syntax("regex must be known at compile time string", self));
36        };
37
38        let regex = regex.as_ref();
39        if regex.is_empty() {
40            return Err(CompileError::syntax("regex cannot be an empty string", self));
41        }
42
43        let re = regex::Regex::new(regex).map_err(|err| CompileError::syntax(format!("bad regex {err}"), self))?;
44
45        let this = this.clone().into_cel()?;
46
47        match this {
48            CompiledExpr::Constant(ConstantCompiledExpr { value }) => {
49                Ok(CompiledExpr::constant(CelValue::cel_matches(value, &re)?))
50            }
51            this => Ok(CompiledExpr::runtime(
52                CelType::Proto(ProtoType::Value(ProtoValueType::Bool)),
53                parse_quote! {{
54                    static REGEX: ::std::sync::LazyLock<::tinc::reexports::regex::Regex> = ::std::sync::LazyLock::new(|| {
55                        ::tinc::reexports::regex::Regex::new(#regex).expect("failed to compile regex this is a bug in tinc")
56                    });
57
58                    ::tinc::__private::cel::CelValue::cel_matches(
59                        #this,
60                        &*REGEX,
61                    )?
62                }},
63            )),
64        }
65    }
66}
67
68#[cfg(test)]
69#[cfg(feature = "prost")]
70#[cfg_attr(coverage_nightly, coverage(off))]
71mod tests {
72    use quote::quote;
73    use syn::parse_quote;
74    use tinc_cel::CelValue;
75
76    use crate::codegen::cel::compiler::{CompiledExpr, Compiler, CompilerCtx};
77    use crate::codegen::cel::functions::{Function, Matches};
78    use crate::codegen::cel::types::CelType;
79    use crate::extern_paths::ExternPaths;
80    use crate::path_set::PathSet;
81    use crate::types::{ProtoType, ProtoTypeRegistry, ProtoValueType};
82
83    #[test]
84    fn test_matches_syntax() {
85        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, ExternPaths::new(crate::Mode::Prost), PathSet::default());
86        let compiler = Compiler::new(&registry);
87        insta::assert_debug_snapshot!(Matches.compile(CompilerCtx::new(compiler.child(), None, &[])), @r#"
88        Err(
89            InvalidSyntax {
90                message: "missing this",
91                syntax: "<this>.matches(<const regex>)",
92            },
93        )
94        "#);
95
96        insta::assert_debug_snapshot!(Matches.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[])), @r#"
97        Err(
98            InvalidSyntax {
99                message: "takes exactly one argument",
100                syntax: "<this>.matches(<const regex>)",
101            },
102        )
103        "#);
104
105        insta::assert_debug_snapshot!(Matches.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[
106            cel_parser::parse("dyn('^h')").unwrap(),
107        ])), @r#"
108        Err(
109            InvalidSyntax {
110                message: "regex must be known at compile time string",
111                syntax: "<this>.matches(<const regex>)",
112            },
113        )
114        "#);
115
116        insta::assert_debug_snapshot!(Matches.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[
117            cel_parser::parse("'^h'").unwrap(),
118        ])), @r"
119        Ok(
120            Constant(
121                ConstantCompiledExpr {
122                    value: Bool(
123                        true,
124                    ),
125                },
126            ),
127        )
128        ");
129    }
130
131    #[test]
132    #[cfg(not(valgrind))]
133    fn test_matches_runtime_string() {
134        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, ExternPaths::new(crate::Mode::Prost), PathSet::default());
135        let compiler = Compiler::new(&registry);
136
137        let string_value =
138            CompiledExpr::runtime(CelType::Proto(ProtoType::Value(ProtoValueType::String)), parse_quote!(input));
139
140        let output = Matches
141            .compile(CompilerCtx::new(
142                compiler.child(),
143                Some(string_value),
144                &[cel_parser::parse("'\\\\d+'").unwrap()],
145            ))
146            .unwrap();
147
148        insta::assert_snapshot!(postcompile::compile_str!(
149            postcompile::config! {
150                test: true,
151                dependencies: vec![
152                    postcompile::Dependency::version("tinc", "*"),
153                ],
154            },
155            quote! {
156                fn matches(input: &String) -> Result<bool, ::tinc::__private::cel::CelError<'_>> {
157                    Ok(#output)
158                }
159
160                #[test]
161                fn test_matches() {
162                    assert_eq!(matches(&"in2dastring".into()).unwrap(), true);
163                    assert_eq!(matches(&"in3dastring".into()).unwrap(), true);
164                    assert_eq!(matches(&"xd".into()).unwrap(), false);
165                }
166            },
167        ));
168    }
169}