Crate validus

source ·
Expand description

validus — validated string slices

validus — validated string slices

use std::sync::OnceLock;

use validus::prelude::*; // vstr, cheap_rule, <&str>.validate(), etc.
use validus::cheap_rule;

use regex::Regex;

const USERNAME_RE_S: &str = r#"^[a-zA-Z0-9_]{1,16}$"#;
static USERNAME_RE: OnceLock<Regex> = OnceLock::new();

// 1. Define your rule.
struct BadUsernameError;
struct UsernameRule;
cheap_rule!(
    UsernameRule,
    err = BadUsernameError,
    msg = "bad username",
    |s: &str| {
        let re = USERNAME_RE.get_or_init(|| Regex::new(USERNAME_RE_S).unwrap());
        re.is_match(s)
    }
);

// 2. Restrict your string slice with the rule.
type Username = vstr<UsernameRule>;

let input = "hello";
let username: Result<&Username, BadUsernameError> = input.validate();
assert!(username.is_ok());
assert_eq!(username.unwrap(), "hello");

let input = "haha 😊";
let username: Result<&Username, _> = input.validate();
assert!(username.is_err());

// Plus, this library has serde support with validation, and more.

This library provides a VStr<Rule> type, which is an un-sized wrapper around regular string slices (str). Since it is un-sized, it can be used as a slice, but it cannot be used as a value. Instead, it is used as a reference to a value.

The library provides inter-conversion between &VStr<_> and other smart pointers such as Box, Rc and Arc. (And, of course, &str).

It also inter-converts with String and exposes the internal string slice with .as_str().

A vstr<_> reference an be compared and hashed with other vstr<_> with possibly different rules and str references using the inner string slice.

(VStr is aliased to vstr for convenience.)

Since vstr<_> compares and hashes the same as str references, they can be used directly as keys in HashMaps and HashSets.

// Illustration: using vstr<_> as a key in a HashMap.

use std::collections::HashMap;

use validus::prelude::*;
use validus::cheap_rule;

struct BadUsernameError;
struct UsernameRule;
cheap_rule!(
    UsernameRule,
    err = BadUsernameError,
    msg = "bad username",
    |s: &str| s.len() <= 16
);

type Username = vstr<UsernameRule>;

let mut map = HashMap::new();
map.insert("hello".validate().unwrap(), 1);
map.insert("world".validate().unwrap(), 2);

// assume_valid bypasses validation, incurring no computational cost,
// so it's useful in this case.
assert_eq!(map.get("hello".assume_valid::<UsernameRule>()), Some(&1));
assert_eq!(map.get("world".assume_valid::<UsernameRule>()), Some(&2));

With the optional serde feature, this crate also supports serialization and deserialization with validation. This means that you can use vstr<_> as a field in a serde-powered struct, and if the input fails the validation, it will be rejected and an error according to the validation rule’s associated Error type will be returned.

  • The serde feature is enabled by default. Disable it using default-features = false in your Cargo.toml to disable it.
// Illustration: a struct with a validated email field.

#[cfg(feature = "serde")] {

use validus::prelude::*;
use validus::cheap_rule;
use serde::Deserialize;

// This rule is very generous. It accepts any string that
// contains an at-symbol.
// (When the error type is not specified, it is inferred to
// be &'static str.)
struct EmailRule;
cheap_rule!(EmailRule, msg = "no at-symbol", |s: &str| s.contains('@'));

#[derive(Deserialize)]
pub struct User {
    pub email: Box<vstr<EmailRule>>,
}

let input = r#"{"email": "notgood"}"#;
let result = serde_json::from_str::<User>(input);
assert!(result.is_err());

let input = r#"{"email": "hi@example.com"}"#;
let result = serde_json::from_str::<User>(input);
assert!(result.is_ok());
assert!(result.unwrap().email.as_str() == "hi@example.com");

}

You are also given the power to override the underlying mechanism using assume_valid. This is useful when you have a vstr<_> that you know is valid, but that is difficult to decide at a given moment. The crate provides check() method that can be used to establish the validity of a vstr<_>.

// Illustration: overriding the validation mechanism.

use validus::prelude::*;
use validus::easy_rule;

struct No;
easy_rule!(No, err = &'static str, |s: &str| Err("i won't accept anything"));

let s = "hello";
let v: &vstr<No> = vstr::assume_valid(s);

// Yup, it works. We overrode the validation mechanism.
assert_eq!(v, "hello");

// But it's not valid. Let's test that.
assert!(v.check().is_err());

(assume_valid is NOT unsafe: vstr makes no further guarantees about the validity of the string slice beyond what str provides. [it also doesn’t make any fewer]. Thus, assume_valid may not be blamed for causing undefined behavior.)

Furthermore, since some pairs of rules can be converted automatically (there is an IMPLIES relation between them), you can use the change_rules associated method to convert a reference to vstr<Rule1> to a reference to vstr<Rule2>. This requires Rule to implement Into<Rule2>. (Otherwise, the regular try_change_rules can be used between any two rules.)

// Illustration: declaring implication.
// Implication means: "Whenever [rule] A says good, so does B."

use validus::prelude::*;
use validus::cheap_rule;

// Less generous
struct A;
cheap_rule!(A, msg = "no wow", |s: &str| s.contains("wow"));

// More generous: includes all strings that A accepts and
// perhaps more.
struct B;
cheap_rule!(B, msg = "neither wow nor bad found", |s: &str| {
    s.contains("wow") || s.contains("bad")
});

// Assert that A implies B.
// In English: "whatever string A accepts, B accepts, too."
impl From<A> for B {
    // This particular formulation is idiomatic
    // to the `validus` crate because all rules are supposed
    // to be freely constructed Zero-Sized Types (ZSTs).
    fn from(_: A) -> Self {
        // And, this value never gets used, anyway.
        // All methods of `ValidateString` (trait that
        // defines rules) have static methods, not instance
        // methods.
        B
    }
}

// The declaration of implication unlocks the `change_rules`
// method that converts a reference to `vstr<A>` to a reference
// to `vstr<B>` infallibly.

let good = "wow bad";
let a: &vstr<A> = vstr::assume_valid(good); // we know it works, so.
let _: &vstr<B> = a.change_rules(); // infallible. see, no Result or unwrap().

Oh, one more. There are two special rules which validate all strings and no strings, respectively. They are called ValidateAll and (). Though you can’t use change_rules to convert your rule to ValidateAll, you can still use a dedicated method called erase_rules just for that. From ValidateAll, you can use try_change_rules to convert to any other rule.

serde with validation

In this example, a string representing email is validated upon deserialization. If it passes the validation, it is wrapped in a Box<Email>; otherwise, an error is returned.

(This example requires the serde feature to be enabled.)

#[cfg(feature = "serde")] {

use validus::prelude::*;
use validus::easy_rule;

use thiserror::Error;

// Here's my rule. My "email" needs to contain an at-symbol somewhere.
// For the demonstration purpose only, I will use only that rule.

#[derive(Debug, Error)]
pub enum EmailError {
    #[error("invalid email")]
    InvalidEmail,
}

struct EmailRule;

easy_rule!(EmailRule, err = EmailError, |s: &str| {
    s.contains('@').then(|| ()).ok_or(EmailError::InvalidEmail)
});

// Now that the rule has been laid out, let me make a type alias to a validated email slice.
type Email = vstr<EmailRule>;

// Great. Now, let me show you an example with acceptance and one with rejection.

// ACCEPTANCE
let goodinput = "my_email@example.com";
assert!(goodinput.validate::<EmailRule>().is_ok());
let s = serde_json::to_string(goodinput).unwrap();
assert!(serde_json::from_str::<Box<Email>>(&s).is_ok());

// REJECTION
let badinput = "my_email";
assert!(badinput.validate::<EmailRule>().is_err());
let s = serde_json::to_string(badinput).unwrap();
assert!(serde_json::from_str::<Box<Email>>(&s).is_err());

}

Deferred validation with Later

Sometimes, you want to validate a string slice only when it is actually used.

For this need, there is a rule called Later that bypasses all validation, but specifies what rule it is supposed to be validated with. When the validation is actually needed, you can call make_strict to validate the string slice and convert it to a vstr with the specified rule.

Here, I copy the example code from the Later type documentation.

use validus::prelude::*;
use validus::cheap_rule;

struct EmailError;
struct Email;
cheap_rule!(Email,
    err = EmailError,
    msg = "no @ symbol",
    |s: &str| s.contains('@')
);

// Here, we start with an email with deferred (postponed) validation.
// Validation of `Later<_>` is infallible.
let v1: &vstr<Later<Email>> = "hi@example.com".validate().unwrap();
// Now, we truly validate it.
let v1: Result<&vstr<Email>, _> = v1.make_strict();
assert!(v1.is_ok());

// So, again, this is going to succeed.
let v2 = "notgood".validate::<Later<Email>>().unwrap();
// But, when we check it, it will fail, since it is not a good email address
// (according to the rule we defined).
let v2 = v2.make_strict();
assert!(v2.is_err());

// With the extension `StrExt`, we can also call `.assume_valid()`
// to skip validation, since we know that `Later<_>` doesn't validate.

let relaxed = "hi@example.com".assume_valid::<Later<Email>>();
assert!(relaxed.check().is_ok()); // This is infallible because `Later<_>` is infallible.
assert!(relaxed.make_strict().is_ok()); // Later<Email> -> Email.

let relaxed = "nonono".assume_valid::<Later<Email>>();
assert!(relaxed.check().is_ok()); // Yup, it is still infallible.
let strict = relaxed.make_strict(); // Now, we made it strict.
assert!(strict.is_err()); // It didn't make it (it was a bad email address.)

… and, more!

  • Check out some of the prepared validation rules in the module vstrext. The module should already have been imported in the prelude module (it’s feature-gated by ext, which is enabled by default.)
  • The experimental cow feature introduces a new type, VCow, which is like Cow<str>, but with optional validation.

Features

  • (default) serde: enables serde support.
  • (default) ext: enables built-in extensions.

Modules

Macros

  • Define a rule using a predicate with ad-hoc errors.
  • Define a rule using a closure.