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
21pub fn 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
40pub fn 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 extract_string<'a>(
55 tester_name: &str,
56 part: &str,
57 value: Option<&'a Value>,
58) -> Result<&'a str> {
59 match value.and_then(Value::as_str) {
60 Some(s) => Ok(s),
61 None => Err(Error::msg(format!(
62 "Tester `{}` was called {} that isn't a string",
63 tester_name, part
64 ))),
65 }
66}
67
68pub fn defined(value: Option<&Value>, params: &[Value]) -> Result<bool> {
70 number_args_allowed("defined", 0, params.len())?;
71
72 Ok(value.is_some())
73}
74
75pub fn undefined(value: Option<&Value>, params: &[Value]) -> Result<bool> {
77 number_args_allowed("undefined", 0, params.len())?;
78
79 Ok(value.is_none())
80}
81
82pub fn string(value: Option<&Value>, params: &[Value]) -> Result<bool> {
84 number_args_allowed("string", 0, params.len())?;
85 value_defined("string", value)?;
86
87 match value {
88 Some(Value::String(_)) => Ok(true),
89 _ => Ok(false),
90 }
91}
92
93pub fn number(value: Option<&Value>, params: &[Value]) -> Result<bool> {
95 number_args_allowed("number", 0, params.len())?;
96 value_defined("number", value)?;
97
98 match value {
99 Some(Value::Number(_)) => Ok(true),
100 _ => Ok(false),
101 }
102}
103
104pub fn odd(value: Option<&Value>, params: &[Value]) -> Result<bool> {
106 number_args_allowed("odd", 0, params.len())?;
107 value_defined("odd", value)?;
108
109 match value.and_then(|v| v.to_number().ok()) {
110 Some(f) => Ok(f % 2.0 != 0.0),
111 _ => Err(Error::msg("Tester `odd` was called on a variable that isn't a number")),
112 }
113}
114
115pub fn even(value: Option<&Value>, params: &[Value]) -> Result<bool> {
117 number_args_allowed("even", 0, params.len())?;
118 value_defined("even", value)?;
119
120 let is_odd = odd(value, params)?;
121 Ok(!is_odd)
122}
123
124pub fn divisible_by(value: Option<&Value>, params: &[Value]) -> Result<bool> {
126 number_args_allowed("divisibleby", 1, params.len())?;
127 value_defined("divisibleby", value)?;
128
129 match value.and_then(|v| v.to_number().ok()) {
130 Some(val) => match params.first().and_then(|v| v.to_number().ok()) {
131 Some(p) => Ok(val % p == 0.0),
132 None => Err(Error::msg(
133 "Tester `divisibleby` was called with a parameter that isn't a number",
134 )),
135 },
136 None => {
137 Err(Error::msg("Tester `divisibleby` was called on a variable that isn't a number"))
138 }
139 }
140}
141
142pub fn iterable(value: Option<&Value>, params: &[Value]) -> Result<bool> {
145 number_args_allowed("iterable", 0, params.len())?;
146 value_defined("iterable", value)?;
147
148 Ok(value.unwrap().is_array() || value.unwrap().is_object())
149}
150
151pub fn object(value: Option<&Value>, params: &[Value]) -> Result<bool> {
154 number_args_allowed("object", 0, params.len())?;
155 value_defined("object", value)?;
156
157 Ok(value.unwrap().is_object())
158}
159
160pub fn starting_with(value: Option<&Value>, params: &[Value]) -> Result<bool> {
162 number_args_allowed("starting_with", 1, params.len())?;
163 value_defined("starting_with", value)?;
164
165 let value = extract_string("starting_with", "on a variable", value)?;
166 let needle = extract_string("starting_with", "with a parameter", params.first())?;
167 Ok(value.starts_with(needle))
168}
169
170pub fn ending_with(value: Option<&Value>, params: &[Value]) -> Result<bool> {
172 number_args_allowed("ending_with", 1, params.len())?;
173 value_defined("ending_with", value)?;
174
175 let value = extract_string("ending_with", "on a variable", value)?;
176 let needle = extract_string("ending_with", "with a parameter", params.first())?;
177 Ok(value.ends_with(needle))
178}
179
180pub fn containing(value: Option<&Value>, params: &[Value]) -> Result<bool> {
182 number_args_allowed("containing", 1, params.len())?;
183 value_defined("containing", value)?;
184
185 match value.unwrap() {
186 Value::String(v) => {
187 let needle = extract_string("containing", "with a parameter", params.first())?;
188 Ok(v.contains(needle))
189 }
190 Value::Array(v) => Ok(v.contains(params.first().unwrap())),
191 Value::Object(v) => {
192 let needle = extract_string("containing", "with a parameter", params.first())?;
193 Ok(v.contains_key(needle))
194 }
195 _ => Err(Error::msg("Tester `containing` can only be used on string, array or map")),
196 }
197}
198
199pub fn matching(value: Option<&Value>, params: &[Value]) -> Result<bool> {
201 number_args_allowed("matching", 1, params.len())?;
202 value_defined("matching", value)?;
203
204 let value = extract_string("matching", "on a variable", value)?;
205 let regex = extract_string("matching", "with a parameter", params.first())?;
206
207 let regex = match Regex::new(regex) {
208 Ok(regex) => regex,
209 Err(err) => {
210 return Err(Error::msg(format!(
211 "Tester `matching`: Invalid regular expression: {}",
212 err
213 )));
214 }
215 };
216
217 Ok(regex.is_match(value))
218}
219
220#[cfg(test)]
221mod tests {
222 use std::collections::HashMap;
223
224 use super::{
225 containing, defined, divisible_by, ending_with, iterable, matching, object, starting_with,
226 string,
227 };
228
229 use serde_json::value::to_value;
230
231 #[test]
232 fn test_number_args_ok() {
233 assert!(defined(None, &[]).is_ok())
234 }
235
236 #[test]
237 fn test_too_many_args() {
238 assert!(defined(None, &[to_value(1).unwrap()]).is_err())
239 }
240
241 #[test]
242 fn test_value_defined() {
243 assert!(string(None, &[]).is_err())
244 }
245
246 #[test]
247 fn test_divisible_by() {
248 let tests = vec![
249 (1.0, 2.0, false),
250 (4.0, 2.0, true),
251 (4.0, 2.1, false),
252 (10.0, 2.0, true),
253 (10.0, 0.0, false),
254 ];
255
256 for (val, divisor, expected) in tests {
257 assert_eq!(
258 divisible_by(Some(&to_value(val).unwrap()), &[to_value(divisor).unwrap()],)
259 .unwrap(),
260 expected
261 );
262 }
263 }
264
265 #[test]
266 fn test_iterable() {
267 assert!(iterable(Some(&to_value(vec!["1"]).unwrap()), &[]).unwrap());
268 assert!(!iterable(Some(&to_value(1).unwrap()), &[]).unwrap());
269 assert!(!iterable(Some(&to_value("hello").unwrap()), &[]).unwrap());
270 }
271
272 #[test]
273 fn test_object() {
274 let mut h = HashMap::new();
275 h.insert("a", 1);
276 assert!(object(Some(&to_value(h).unwrap()), &[]).unwrap());
277 assert!(!object(Some(&to_value(1).unwrap()), &[]).unwrap());
278 assert!(!object(Some(&to_value("hello").unwrap()), &[]).unwrap());
279 }
280
281 #[test]
282 fn test_starting_with() {
283 assert!(starting_with(
284 Some(&to_value("helloworld").unwrap()),
285 &[to_value("hello").unwrap()],
286 )
287 .unwrap());
288 assert!(
289 !starting_with(Some(&to_value("hello").unwrap()), &[to_value("hi").unwrap()],).unwrap()
290 );
291 }
292
293 #[test]
294 fn test_ending_with() {
295 assert!(
296 ending_with(Some(&to_value("helloworld").unwrap()), &[to_value("world").unwrap()],)
297 .unwrap()
298 );
299 assert!(
300 !ending_with(Some(&to_value("hello").unwrap()), &[to_value("hi").unwrap()],).unwrap()
301 );
302 }
303
304 #[test]
305 fn test_containing() {
306 let mut map = HashMap::new();
307 map.insert("hey", 1);
308
309 let tests = vec![
310 (to_value("hello world").unwrap(), to_value("hel").unwrap(), true),
311 (to_value("hello world").unwrap(), to_value("hol").unwrap(), false),
312 (to_value(vec![1, 2, 3]).unwrap(), to_value(3).unwrap(), true),
313 (to_value(vec![1, 2, 3]).unwrap(), to_value(4).unwrap(), false),
314 (to_value(map.clone()).unwrap(), to_value("hey").unwrap(), true),
315 (to_value(map.clone()).unwrap(), to_value("ho").unwrap(), false),
316 ];
317
318 for (container, needle, expected) in tests {
319 assert_eq!(containing(Some(&container), &[needle]).unwrap(), expected);
320 }
321 }
322
323 #[test]
324 fn test_matching() {
325 let tests = vec![
326 (to_value("abc").unwrap(), to_value("b").unwrap(), true),
327 (to_value("abc").unwrap(), to_value("^b$").unwrap(), false),
328 (
329 to_value("Hello, World!").unwrap(),
330 to_value(r"(?i)(hello\W\sworld\W)").unwrap(),
331 true,
332 ),
333 (
334 to_value("The date was 2018-06-28").unwrap(),
335 to_value(r"\d{4}-\d{2}-\d{2}$").unwrap(),
336 true,
337 ),
338 ];
339
340 for (container, needle, expected) in tests {
341 assert_eq!(matching(Some(&container), &[needle]).unwrap(), expected);
342 }
343
344 assert!(
345 matching(Some(&to_value("").unwrap()), &[to_value("(Invalid regex").unwrap()]).is_err()
346 );
347 }
348}