1use crate::context::ValueNumber;
2use crate::errors::{Error, Result};
3use regex::Regex;
4use serde_json::value::Value;
5
6pub trait Test: Sync + Send {
8 fn test(&self, value: Option<&Value>, args: &[Value]) -> Result<bool>;
10}
11
12impl<F> Test for F
13where
14 F: Fn(Option<&Value>, &[Value]) -> Result<bool> + Sync + Send,
15{
16 fn test(&self, value: Option<&Value>, args: &[Value]) -> Result<bool> {
17 self(value, args)
18 }
19}
20
21fn number_args_allowed(tester_name: &str, max: usize, args_len: usize) -> Result<()> {
23 if max == 0 && args_len > max {
24 return Err(Error::msg(format!(
25 "Tester `{}` was called with some args but this test doesn't take args",
26 tester_name
27 )));
28 }
29
30 if args_len > max {
31 return Err(Error::msg(format!(
32 "Tester `{}` was called with {} args, the max number is {}",
33 tester_name, args_len, max
34 )));
35 }
36
37 Ok(())
38}
39
40fn value_defined(tester_name: &str, value: Option<&Value>) -> Result<()> {
42 if value.is_none() {
43 return Err(Error::msg(format!(
44 "Tester `{}` was called on an undefined variable",
45 tester_name
46 )));
47 }
48
49 Ok(())
50}
51
52pub fn defined(value: Option<&Value>, params: &[Value]) -> Result<bool> {
54 number_args_allowed("defined", 0, params.len())?;
55
56 Ok(value.is_some())
57}
58
59pub fn undefined(value: Option<&Value>, params: &[Value]) -> Result<bool> {
61 number_args_allowed("undefined", 0, params.len())?;
62
63 Ok(value.is_none())
64}
65
66pub fn string(value: Option<&Value>, params: &[Value]) -> Result<bool> {
68 number_args_allowed("string", 0, params.len())?;
69 value_defined("string", value)?;
70
71 match value {
72 Some(Value::String(_)) => Ok(true),
73 _ => Ok(false),
74 }
75}
76
77pub fn number(value: Option<&Value>, params: &[Value]) -> Result<bool> {
79 number_args_allowed("number", 0, params.len())?;
80 value_defined("number", value)?;
81
82 match value {
83 Some(Value::Number(_)) => Ok(true),
84 _ => Ok(false),
85 }
86}
87
88pub fn odd(value: Option<&Value>, params: &[Value]) -> Result<bool> {
90 number_args_allowed("odd", 0, params.len())?;
91 value_defined("odd", value)?;
92
93 match value.and_then(|v| v.to_number().ok()) {
94 Some(f) => Ok(f % 2.0 != 0.0),
95 _ => Err(Error::msg("Tester `odd` was called on a variable that isn't a number")),
96 }
97}
98
99pub fn even(value: Option<&Value>, params: &[Value]) -> Result<bool> {
101 number_args_allowed("even", 0, params.len())?;
102 value_defined("even", value)?;
103
104 let is_odd = odd(value, params)?;
105 Ok(!is_odd)
106}
107
108pub fn divisible_by(value: Option<&Value>, params: &[Value]) -> Result<bool> {
110 number_args_allowed("divisibleby", 1, params.len())?;
111 value_defined("divisibleby", value)?;
112
113 match value.and_then(|v| v.to_number().ok()) {
114 Some(val) => match params.first().and_then(|v| v.to_number().ok()) {
115 Some(p) => Ok(val % p == 0.0),
116 None => Err(Error::msg(
117 "Tester `divisibleby` was called with a parameter that isn't a number",
118 )),
119 },
120 None => {
121 Err(Error::msg("Tester `divisibleby` was called on a variable that isn't a number"))
122 }
123 }
124}
125
126pub fn iterable(value: Option<&Value>, params: &[Value]) -> Result<bool> {
129 number_args_allowed("iterable", 0, params.len())?;
130 value_defined("iterable", value)?;
131
132 Ok(value.unwrap().is_array())
133}
134
135fn extract_string<'a>(tester_name: &str, part: &str, value: Option<&'a Value>) -> Result<&'a str> {
138 match value.and_then(|v| v.as_str()) {
139 Some(s) => Ok(s),
140 None => Err(Error::msg(format!(
141 "Tester `{}` was called {} that isn't a string",
142 tester_name, part
143 ))),
144 }
145}
146
147pub fn starting_with(value: Option<&Value>, params: &[Value]) -> Result<bool> {
149 number_args_allowed("starting_with", 1, params.len())?;
150 value_defined("starting_with", value)?;
151
152 let value = extract_string("starting_with", "on a variable", value)?;
153 let needle = extract_string("starting_with", "with a parameter", params.first())?;
154 Ok(value.starts_with(needle))
155}
156
157pub fn ending_with(value: Option<&Value>, params: &[Value]) -> Result<bool> {
159 number_args_allowed("ending_with", 1, params.len())?;
160 value_defined("ending_with", value)?;
161
162 let value = extract_string("ending_with", "on a variable", value)?;
163 let needle = extract_string("ending_with", "with a parameter", params.first())?;
164 Ok(value.ends_with(needle))
165}
166
167pub fn containing(value: Option<&Value>, params: &[Value]) -> Result<bool> {
169 number_args_allowed("containing", 1, params.len())?;
170 value_defined("containing", value)?;
171
172 match value.unwrap() {
173 Value::String(v) => {
174 let needle = extract_string("containing", "with a parameter", params.first())?;
175 Ok(v.contains(needle))
176 }
177 Value::Array(v) => Ok(v.contains(params.first().unwrap())),
178 Value::Object(v) => {
179 let needle = extract_string("containing", "with a parameter", params.first())?;
180 Ok(v.contains_key(needle))
181 }
182 _ => Err(Error::msg("Tester `containing` can only be used on string, array or map")),
183 }
184}
185
186pub fn matching(value: Option<&Value>, params: &[Value]) -> Result<bool> {
188 number_args_allowed("matching", 1, params.len())?;
189 value_defined("matching", value)?;
190
191 let value = extract_string("matching", "on a variable", value)?;
192 let regex = extract_string("matching", "with a parameter", params.first())?;
193
194 let regex = match Regex::new(regex) {
195 Ok(regex) => regex,
196 Err(err) => {
197 return Err(Error::msg(format!(
198 "Tester `matching`: Invalid regular expression: {}",
199 err
200 )));
201 }
202 };
203
204 Ok(regex.is_match(value))
205}
206
207#[cfg(test)]
208mod tests {
209 use std::collections::HashMap;
210
211 use super::{
212 containing, defined, divisible_by, ending_with, iterable, matching, starting_with, string,
213 };
214
215 use serde_json::value::to_value;
216
217 #[test]
218 fn test_number_args_ok() {
219 assert!(defined(None, &vec![]).is_ok())
220 }
221
222 #[test]
223 fn test_too_many_args() {
224 assert!(defined(None, &vec![to_value(1).unwrap()]).is_err())
225 }
226
227 #[test]
228 fn test_value_defined() {
229 assert!(string(None, &[]).is_err())
230 }
231
232 #[test]
233 fn test_divisible_by() {
234 let tests = vec![
235 (1.0, 2.0, false),
236 (4.0, 2.0, true),
237 (4.0, 2.1, false),
238 (10.0, 2.0, true),
239 (10.0, 0.0, false),
240 ];
241
242 for (val, divisor, expected) in tests {
243 assert_eq!(
244 divisible_by(Some(&to_value(val).unwrap()), &[to_value(divisor).unwrap()],)
245 .unwrap(),
246 expected
247 );
248 }
249 }
250
251 #[test]
252 fn test_iterable() {
253 assert_eq!(iterable(Some(&to_value(vec!["1"]).unwrap()), &[]).unwrap(), true);
254 assert_eq!(iterable(Some(&to_value(1).unwrap()), &[]).unwrap(), false);
255 assert_eq!(iterable(Some(&to_value("hello").unwrap()), &[]).unwrap(), false);
256 }
257
258 #[test]
259 fn test_starting_with() {
260 assert!(starting_with(
261 Some(&to_value("helloworld").unwrap()),
262 &[to_value("hello").unwrap()],
263 )
264 .unwrap());
265 assert!(
266 !starting_with(Some(&to_value("hello").unwrap()), &[to_value("hi").unwrap()],).unwrap()
267 );
268 }
269
270 #[test]
271 fn test_ending_with() {
272 assert!(
273 ending_with(Some(&to_value("helloworld").unwrap()), &[to_value("world").unwrap()],)
274 .unwrap()
275 );
276 assert!(
277 !ending_with(Some(&to_value("hello").unwrap()), &[to_value("hi").unwrap()],).unwrap()
278 );
279 }
280
281 #[test]
282 fn test_containing() {
283 let mut map = HashMap::new();
284 map.insert("hey", 1);
285
286 let tests = vec![
287 (to_value("hello world").unwrap(), to_value("hel").unwrap(), true),
288 (to_value("hello world").unwrap(), to_value("hol").unwrap(), false),
289 (to_value(vec![1, 2, 3]).unwrap(), to_value(3).unwrap(), true),
290 (to_value(vec![1, 2, 3]).unwrap(), to_value(4).unwrap(), false),
291 (to_value(map.clone()).unwrap(), to_value("hey").unwrap(), true),
292 (to_value(map.clone()).unwrap(), to_value("ho").unwrap(), false),
293 ];
294
295 for (container, needle, expected) in tests {
296 assert_eq!(containing(Some(&container), &[needle]).unwrap(), expected);
297 }
298 }
299
300 #[test]
301 fn test_matching() {
302 let tests = vec![
303 (to_value("abc").unwrap(), to_value("b").unwrap(), true),
304 (to_value("abc").unwrap(), to_value("^b$").unwrap(), false),
305 (
306 to_value("Hello, World!").unwrap(),
307 to_value(r"(?i)(hello\W\sworld\W)").unwrap(),
308 true,
309 ),
310 (
311 to_value("The date was 2018-06-28").unwrap(),
312 to_value(r"\d{4}-\d{2}-\d{2}$").unwrap(),
313 true,
314 ),
315 ];
316
317 for (container, needle, expected) in tests {
318 assert_eq!(matching(Some(&container), &[needle]).unwrap(), expected);
319 }
320
321 assert!(
322 matching(Some(&to_value("").unwrap()), &[to_value("(Invalid regex").unwrap()]).is_err()
323 );
324 }
325}