Skip to main content

usage_validation/
lib.rs

1//! Cold-path, portable validation of values declared by a usage spec.
2//!
3//! Expressions use the language implemented by both `jdx/expr.rs` and the original
4//! `expr-lang/expr` Go package. The spec supplies a raw CLI value as `value`, so explicit
5//! conversions such as `int(value)` have the same meaning in every runtime.
6//!
7//! # What this build of the language carries
8//!
9//! A `validate=` expression reads one string and returns a boolean, so this depends on
10//! `expr-lang` with its default features off — the dates, JSON codec and base64 it ships by
11//! default are twelve crates a CLI would compile to evaluate `int(value) > 0`. The operators,
12//! the string and array builtins, the numeric conversions and `matches` are all here.
13//!
14//! `matches` is kept deliberately: the Go runtime has it, and a spec is supposed to mean the
15//! same thing in both.
16//!
17//! A spec that does want the rest — `date(value) > now()`, `fromJSON(value)` — gets it by
18//! naming the feature in the *application's* manifest, which cargo unifies with this crate's
19//! copy:
20//!
21//! ```toml
22//! expr-lang = { version = "2.1", features = ["temporal"] }
23//! ```
24//!
25//! Without that, such an expression fails the same way `nosuchfn(value)` always has: not at
26//! [`check`] time, which only parses, but when a value reaches it — with a message naming the
27//! feature to add.
28
29#![forbid(unsafe_code)]
30
31use expr::{Context, Value};
32
33/// Check that a validation declaration is syntactically valid without evaluating it.
34pub fn check(expression: &str) -> Result<(), String> {
35    expr::compile(expression)
36        .map(|_| ())
37        .map_err(|error| error.to_string())
38}
39
40/// Evaluate a validation expression for one raw CLI value.
41///
42/// A valid expression must return a boolean. `false` means the value does not satisfy the
43/// declaration; an evaluator error or a non-boolean result means the declaration itself is
44/// invalid.
45pub fn validate(expression: &str, value: &str) -> Result<bool, String> {
46    let mut context = Context::default();
47    context.insert("value", value);
48    match expr::eval(expression, &context).map_err(|error| error.to_string())? {
49        Value::Bool(valid) => Ok(valid),
50        result => Err(format!(
51            "validation expression must return a boolean, got {result}"
52        )),
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::{check, validate};
59
60    #[test]
61    fn checks_syntax_without_needing_a_sample_value() {
62        assert_eq!(check("int(value) > 0"), Ok(()));
63        assert!(check("int(value) >").is_err());
64    }
65
66    #[test]
67    fn validates_raw_values_with_expr_conversions() {
68        let expression = "int(value) >= 1 && int(value) <= 65535";
69        assert_eq!(validate(expression, "9229"), Ok(true));
70        assert_eq!(validate(expression, "0"), Ok(false));
71        assert!(validate(expression, "not-a-port").is_err());
72    }
73
74    #[test]
75    fn requires_a_boolean_result() {
76        let error = validate("int(value)", "42").unwrap_err();
77        assert!(error.contains("must return a boolean"), "{error}");
78    }
79}