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::types::{ProtoType, ProtoTypeRegistry, ProtoValueType};
80
81    #[test]
82    fn test_matches_syntax() {
83        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, crate::extern_paths::ExternPaths::new(crate::Mode::Prost));
84        let compiler = Compiler::new(&registry);
85        insta::assert_debug_snapshot!(Matches.compile(CompilerCtx::new(compiler.child(), None, &[])), @r#"
86        Err(
87            InvalidSyntax {
88                message: "missing this",
89                syntax: "<this>.matches(<const regex>)",
90            },
91        )
92        "#);
93
94        insta::assert_debug_snapshot!(Matches.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[])), @r#"
95        Err(
96            InvalidSyntax {
97                message: "takes exactly one argument",
98                syntax: "<this>.matches(<const regex>)",
99            },
100        )
101        "#);
102
103        insta::assert_debug_snapshot!(Matches.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[
104            cel_parser::parse("dyn('^h')").unwrap(),
105        ])), @r#"
106        Err(
107            InvalidSyntax {
108                message: "regex must be known at compile time string",
109                syntax: "<this>.matches(<const regex>)",
110            },
111        )
112        "#);
113
114        insta::assert_debug_snapshot!(Matches.compile(CompilerCtx::new(compiler.child(), Some(CompiledExpr::constant(CelValue::String("hi".into()))), &[
115            cel_parser::parse("'^h'").unwrap(),
116        ])), @r"
117        Ok(
118            Constant(
119                ConstantCompiledExpr {
120                    value: Bool(
121                        true,
122                    ),
123                },
124            ),
125        )
126        ");
127    }
128
129    #[test]
130    #[cfg(not(valgrind))]
131    fn test_matches_runtime_string() {
132        let registry = ProtoTypeRegistry::new(crate::Mode::Prost, crate::extern_paths::ExternPaths::new(crate::Mode::Prost));
133        let compiler = Compiler::new(&registry);
134
135        let string_value =
136            CompiledExpr::runtime(CelType::Proto(ProtoType::Value(ProtoValueType::String)), parse_quote!(input));
137
138        let output = Matches
139            .compile(CompilerCtx::new(
140                compiler.child(),
141                Some(string_value),
142                &[cel_parser::parse("'\\\\d+'").unwrap()],
143            ))
144            .unwrap();
145
146        insta::assert_snapshot!(postcompile::compile_str!(
147            postcompile::config! {
148                test: true,
149                dependencies: vec![
150                    postcompile::Dependency::version("tinc", "*"),
151                ],
152            },
153            quote! {
154                fn matches(input: &String) -> Result<bool, ::tinc::__private::cel::CelError<'_>> {
155                    Ok(#output)
156                }
157
158                #[test]
159                fn test_matches() {
160                    assert_eq!(matches(&"in2dastring".into()).unwrap(), true);
161                    assert_eq!(matches(&"in3dastring".into()).unwrap(), true);
162                    assert_eq!(matches(&"xd".into()).unwrap(), false);
163                }
164            },
165        ));
166    }
167}