tinc_build/codegen/cel/functions/
matches.rs1use 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
12impl 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(®istry);
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(®istry);
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}