Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/axionbuster/validus

Validated string slices
https://github.com/axionbuster/validus

rust rust-patterns validation

Last synced: about 1 month ago
JSON representation

Validated string slices

Awesome Lists containing this project

README

        

# `validus` --- validated string slices

```rust
// A three-step process.

// 1. Define your validation. No need to re-implement your validation logic!
// (though, we have a macro that makes it easy if you do have control.)
// Validus offers multiple tradeoff points between flexibility and convenience.

// 2. Replace `str` with `vstr` where you need validation.
// `&vstr` coerces to `&str`, so you can use it deep inside your codebase,
// or even a foreign codebase. (Limitations exist.)

// 3. Use `vstr` as though it were `str`.

# use std::sync::OnceLock;
#
use validus::prelude::*; // vstr, fast_rule, <&str>.validate(), etc.
use validus::fast_rule;

#
# use regex::Regex;
#
// Let's define a string validation rule that uses a Regex.
const USERNAME_RE_S: &str = r#"^[a-zA-Z0-9_]{1,16}$"#;
static USERNAME_RE: OnceLock = OnceLock::new();

// 1. Defining the rule type and the error type.
// - I used fast_rule! because I have control over the error type.
// - If you are using existing error types and/or function,
// you can use easy_rule! instead. There's also a macro-free way;
// see the `vstr` module docs for more info.
struct BadUsernameError;
struct UsernameRule;
fast_rule!(
UsernameRule,
err = BadUsernameError,
msg = "bad username",
// The closure below could very well be a function.
// So if the function were called `username_rule`,
// I could do fast_rule!(..., ..., ..., username_rule)
|s: &str| {
let re = USERNAME_RE.get_or_init(|| Regex::new(USERNAME_RE_S).unwrap());
re.is_match(s)
}
);
// (There's also easy_rule that allows you to bring your own error type.)
// (That's helpful when you have existing error types.)
// (... or, walk the macro-free path and implement ValidateString manually.)
// (consult the `vstr` module docs for more info.)

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

// 3. Use your `vstr` as though it were `str`.
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());

// It's that easy! Let's do a recap.

// - Error and the rule:
// struct MyError;
// struct MyRule;
// fn my_rule_priv(s: &str) -> Result<(), MyError> { ... }
// fast_rule!(MyRule, err = MyError, msg = "my error", my_rule_priv);

// - Use `vstr` as though it were `str`:
// type MyStr = vstr;
// let a: &vstr = "hello".validate().unwrap();

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

## Synopsis

1. **Bring your own validation**, and use [`vstr<_>`](crate::vstr::vstr)
as though it were `str` (where immutable `str` references are expected).
2. **Mix it into your code.** If you have existing validation modules,
don't touch it. Validus has multiple easy ways to wrap your validation
into a `vstr<_>`-compatible rule type.
3. **Take a shortcut when starting out.** If you don't have existing validation
modules, you can use the [`fast_rule!`](crate::fast_rule) macro to quickly define a rule type
with a predicate (closure or external function, which may or may not
belong to your crate). Your error type will be a proper `std::error::Error`
type; you can use an ad-hoc `&'static str` error message, even.
3. **Work with `serde` with no extra code.**
Simply add `#[derive(Serialize, Deserialize)]` to your struct as usual.
Now, no need to worry about invalid strings getting deserialized.
4. **Opt into lazy validation if you want.**
With a special rule called [`Later<_>`](crate::vstr::Later), you can also choose to either eagerly
validate or defer validation until when you decide you need it.
(Pretty useful for `serde` deserialization, for example.)
5. **Combine rules on the spot.** Three logical connectives (and, or, not) are provided
as an extension enabled by default; those are in your prelude, so you can
use them right away.

The Validus library provides a newtype over regular string slices
called `vstr`. This `vstr` is parameterized by your own validation
rule type, like `vstr`. Any zero-sized type can be used
as a rule if it implements the trait [`ValidateString`](crate::vstr::ValidateString).

`vstr` is meant to be a replacement of `str` in certain contexts.
So,
- `vstr` implements `Eq` with `vstr` of other rules as well as `str`,
- ... and `Ord` and `Hash` the same as any other `str` reference.
- `vstr` is aware of `String`, `Cow` and smart pointer types
such as `Box`, `Rc` and `Arc`.

To show you how `vstr<_>` compares and hashes the same as `str` references,
I will give you an example of directly using `vstr` as keys in `HashMap`s and `HashSet`s.

```rust
// Illustration: using vstr<_> as a key in a HashMap.
#
# use std::collections::HashMap;
#
# use validus::prelude::*;
# use validus::fast_rule;

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

type Username = vstr;

let mut map = HashMap::<&Username, i32>::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()), Some(&1));
assert_eq!(map.get("world".assume_valid()), Some(&2));

// So every time you need a `&str` but with validation,
// you know that Validus and `vstr<_>` have got you covered,
// anywhere in your codebase, or a foreign codebase.
// (Limitations exist.)
```

## `serde` with validation

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`.

```rust
// Illustration: a struct with a validated email field.

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

use validus::prelude::*;
use validus::fast_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;
fast_rule!(EmailRule, msg = "no at-symbol", |s: &str| s.contains('@'));

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

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

let input = r#"{"email": "[email protected]"}"#;
let result = serde_json::from_str::(input);
assert!(result.is_ok());
assert!(result.unwrap().email.as_str() == "[email protected]");

}
```

## Deferred validation with [`Later`](crate::vstr::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.

- [`Later`](crate::vstr::Later)
- [`make_strict`](crate::vstr::VStr::make_strict)
- [`assume_valid`](crate::vstr::VStr::assume_valid)

```rust
use validus::prelude::*;
use validus::fast_rule;

struct EmailError;
struct Email;
fast_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> = "[email protected]".validate().unwrap();
// Now, we truly validate it.
let v1: Result<&vstr, _> = v1.make_strict();
assert!(v1.is_ok());

// So, again, this is going to succeed.
let v2 = "notgood".validate::>().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 = "[email protected]".assume_valid::>();
assert!(relaxed.check().is_ok()); // This is infallible because `Later<_>` is infallible.
assert!(relaxed.make_strict().is_ok()); // Later -> Email.

let relaxed = "nonono".assume_valid::>();
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.)
```

## Overriding a rule with `assume_valid` and checking with `check`

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; or, when, for some reason, you don't need
to validate a `vstr<_>` (for example, when you are using it as a
look-up key in a `HashMap`).
The crate provides the `check()` method that can be used to
establish the validity of a `vstr<_>`.

- [`assume_valid`](crate::vstr::VStr::assume_valid)
- [`check`](crate::vstr::VStr::check)

```rust
// Illustration: overriding the validation mechanism.

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

// easy_rule is a different macro that helps you define rules.
// The difference with fast_rule! is that leaves the error type
// untouched, and you need to return a Result<(), YourError>
// instead of a bool in the closure.
struct No;
easy_rule!(No, err = &'static str, |s: &str| Err("i won't accept anything"));

let s = "hello";
let v: &vstr = 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.**)

## Defining implication among rules

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` to a reference to
`vstr`. This requires `Rule` to implement `Into`.
(Otherwise, the regular `try_change_rules` can be used
between any two rules.)

- [`change_rules`](crate::vstr::VStr::change_rules)
- [`try_change_rules`](crate::vstr::VStr::try_change_rules)
- [`ValidateString`](crate::vstr::ValidateString)
- [`Into`] and [`From`]

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

use validus::prelude::*;
use validus::fast_rule;

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

// More generous: includes all strings that A accepts and
// perhaps more.
struct B;
fast_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 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
` to a reference
// to `vstr` infallibly.

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

### The special rule `ValidateAll`

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.

- [`try_change_rules`](crate::vstr::VStr::try_change_rules)
- [`change_rules`](crate::vstr::VStr::change_rules)
- [`erase_rules`](crate::vstr::VStr::erase_rules)
- [`ValidateAll`](crate::vstr::ValidateAll)

## Batteries included ... (but I need your help!)

Check out some of the prepared validation rules in the module [`vstrext`][crate::vstrext].
The module should already have been imported in the prelude module
(it's feature-gated by `ext`, which is enabled by default.)

Currently, three *logical connectives* and a few rules are implemented in the extension module:
- [`Conj`](crate::vstrext::Conj), which requires two rules to be satisfied.
- [`Disj`](crate::vstrext::Disj), which requires at least one of two rules to be satisfied.
- [`Neg`](crate::vstrext::Neg), which requires a rule to be **un**-satisfied.
- [`StringSizeRule`](crate::vstrext::StringSizeRule) (and its variants), which check the size of a string.
- [`StringAsciiRule`](crate::vstrext::StringAsciiRule), our only rule that checks the content of a string so far.

I would really appreciate your help in adding more rules to the extension module.

## Experimental features

### Contingent validation

The experimental `cow` feature introduces a new type, `VCow` that represents
a `Cow<'_, vstr<_>>` that is either valid or invalid. The validity is tracked
at runtime.

**NOTE**: `vstr` already supports being in a Cow like this: `Cow<'_, vstr<_>>`
even without the `cow` feature. The `cow` feature adds an experimental
wrapper type that tracks the validity of the `vstr` that may change at runtime.

- [`VCow`](crate::vstr::VCow)

## Features

- (default) `serde`: enables `serde` support. Pulls in `serde` as a dependency.
- (default) `ext`: enables built-in extensions. Pulls in `thiserror` as a dependency.
- (default) `std`: enables `std` support. Necessary to implement the `Error` trait. Pulls in `std` as a dependency.
- (default) `alloc`: integrates with `alloc` crate., and enables smart pointers.
- `cow`: enables the experimental `VCow` type. Pulls in `alloc` as a dependency.