5 releases

new 0.1.0 Jan 7, 2025
0.0.4 Jan 5, 2025
0.0.3 Jan 4, 2025
0.0.2 Jan 4, 2025
0.0.1 Jan 1, 2025

#35 in No standard library

Download history 93/week @ 2024-12-26 335/week @ 2025-01-02

428 downloads per month

MIT/Apache

140KB
4K SLoC

Wary

https://img.shields.io/crates/v/wary https://img.shields.io/docsrs/wary ci status

An optionally no_std and no_alloc validation and transformation library.

Basic struct example

use std::borrow::Cow;
use wary::Wary;

#[derive(Wary)]
struct Name<'n>(
  #[validate(alphanumeric, length(chars, 5..=20), equals(not, other = "john"))]
  Cow<'n, str>
);

#[derive(Wary)]
struct Person<'n> {
  #[validate(dive)]
  name: Name<'n>,
  #[validate(range(..=100))]
  age: u8,
}

let mut person = Person {
  name: Name(Cow::Borrowed("jane")),
  age: 25,
};

if let Err(report) = person.wary(&()) {
  eprintln!("invalid person: {report:?}");
}

Basic enum example

use std::borrow::Cow;
use wary::Wary;

#[derive(Wary)]
struct Name<'n>(
  #[validate(alphanumeric, length(chars, 5..=20), equals(not, other = "john"))]
  #[transform(lowercase(ascii))]
  &'n mut str
);

// for length(bytes)
impl wary::AsRef<[u8]> for Name<'_> {
  fn as_ref(&self) -> &[u8] {
    self.0.as_bytes()
  }
}

#[derive(Wary)]
enum Person<'n> {
  Child {
    #[validate(dive)]
    name: Name<'n>,
    #[validate(range(..=17))]
    age: u8,
  },
  Adult {
    #[validate(dive, length(bytes, ..=32))]
    name: Name<'n>,
    #[validate(range(18..=100))]
    age: u8,
  },
}

let mut name = "Jane".to_string();
let mut person = Person::Adult {
  name: Name(&mut name),
  age: 25,
};

if let Err(report) = person.wary(&()) {
  eprintln!("invalid person: {report:?}");
} else {
  let Person::Adult { name, age } = person else {
    unreachable!();
  };

  assert_eq!(name.0, "jane");
}

Accessing context

use wary::Wary;
use wary::toolbox::rule::*;
use std::ops::Range;

// allows one context to be passed to all rules
#[derive(AsRef)]
struct Context {
  range: Range<u8>,
  #[as_ref(skip)]
  useless: bool,
}

struct RangeRule<C> {
  ctx: PhantomData<C>,
}

impl<C> RangeRule<C> {
  fn new() -> Self {
    Self {
      ctx: PhantomData,
    }
  }
}

impl<C> wary::Rule<u8> for RangeRule<C>
where
  C: AsRef<Range<u8>>,
{
  type Context = C;

  fn validate(&self, ctx: &Self::Context, item: &u8) -> Result<()> {
    if ctx.as_ref().contains(item) {
      Ok(())
    } else {
      Err(wary::Error::with_message("out_of_range", "The number is out of range"))
    }
  }
}

#[allow(non_camel_case_types)]
mod rule {
  pub type range<C> = super::RangeRule<C>;
}

#[derive(Wary)]
#[wary(context = Context)]
struct Age {
  #[validate(custom(range))]
  number: u8,
}

# fn main() {}

Validation rules

Validation rules applied through the proc-macro Wary attribute are (for the most part) simply forwarded directly to their respective builders inside the rule module. As a result of this decision, all rules (except and, or, inner, and dive) will have auto-completion when writing macro attributes!

If you're providing no options to a rule, you can omit the parentheses. For example: #[validate(alphanumeric)] and #[validate(alphanumeric())] are equivalent.

rule trait feature dependency
addr AsRef<str> - -
alphanumeric AsRef<str> - -
and - - -
ascii AsRef<str> - -
contains AsSlice - -
credit_card AsRef<str> credit_card creditcard
custom Rule<T> - -
dive Validate - -
email AsRef<str> email email_address
equals std::cmp::PartialEq - -
func Fn(&T) -> Result<(), wary::Error> - -
inner AsSlice - -
length Length graphemes* unicode-segmentation
lowercase AsRef<str> - -
or - - -
prefix AsSlice - -
range Compare - -
regex AsRef<str> regex regex
required AsSlice - -
semver AsRef<str> semver semver
suffix AsSlice - -
uppercase AsRef<str> - -
url AsRef<str> url url
uuid AsRef<str> uuid uuid

* optional

addr

Validates an address (currently only an IP).

use wary::Wary;

#[derive(Wary)]
struct Packet {
  #[validate(addr(ipv4))]
  src: String,
  #[validate(addr(ipv6))]
  dest: String,
  #[validate(addr(ip))]
  more: String,
}

alphanumeric

Validates that the input is alphanumeric.

use wary::Wary;

#[derive(Wary)]
struct Name {
  #[validate(alphanumeric)]
  left: String,
  #[validate(alphanumeric(ascii))]
  right: String,
}

and

Meta-rule that combines multiple rules. Unlike other rule lists, this one short-circuits on the first error.

use wary::{Wary, Validate};

#[derive(Wary)]
struct NameAnd {
  #[validate(and(equals(other = 1), range(2..=2)))]
  value: u8
}

let name = NameAnd {
  value: 3,
};

let report = name.validate(&()).unwrap_err();

assert_eq!(report.len(), 1);

#[derive(Wary)]
struct Name {
  #[validate(equals(other = 1), range(2..=2))]
  value: u8
}

let name = Name {
  value: 3,
};

let report = name.validate(&()).unwrap_err();

assert_eq!(report.len(), 2);

ascii

Validates that the input is ascii.

use wary::Wary;

#[derive(Wary)]
struct Name(
  #[validate(ascii)]
  String
);

contains

Validates that the input contains a substring or subslice.

use wary::Wary;

#[derive(Wary)]
struct Name(
  #[validate(contains(str = "hello"))]
  String
);

credit_card (requires feature credit_card)

Validates that the input is a credit card number (PAN).

use wary::Wary;

#[derive(Wary)]
struct Card(
  #[validate(credit_card)]
  String
);

custom

Validates the input with a custom Rule.

use wary::Wary;
use wary::toolbox::rule::*;

struct SecretRule;

impl SecretRule {
  fn new() -> Self {
    Self
  }
}

impl<I> wary::Rule<I> for SecretRule
where
  I: AsRef<str>,
{
  type Context = ();

  fn validate(&self, _ctx: &Self::Context, item: &I) -> Result<()> {
    let string = item.as_ref();

    if string.contains("secret") {
      Err(Error::with_message("secret_found", "You cannot use the word 'secret'"))
    } else {
      Ok(())
    }
  }
}

#[allow(non_camel_case_types)]
mod rule {
  pub type secret = super::SecretRule;
}

#[derive(Wary)]
struct Person {
  #[validate(custom(secret))]
  name: String,
}

# fn main() {}

dive

Validates the inner fields of a struct or enum.

use wary::Wary;

#[derive(Wary)]
struct Item {
  #[validate(ascii)]
  name: &'static str,
}

#[derive(Wary)]
struct Name {
  #[validate(dive)]
  item: Item,
}

email (requires feature email)

Validates that the input is an email.

use wary::Wary;

#[derive(Wary)]
struct Email(
  #[validate(email)]
  String
);

equals

Validates that the input is equal to a value. Currently does not support self fields.

use wary::Wary;

#[derive(Wary)]
struct Name(
  #[validate(equals(other = "John"))]
  String
);

func

Validates the input with a function.

use wary::{Wary, Error};

fn check(_ctx: &(), name: &str) -> Result<(), Error> {
  if name.len() > 5 {
    Ok(())
  } else {
    Err(Error::with_message("name_too_short", "Your name must be longer than 5 characters"))
  }
}

#[derive(Wary)]
struct Name {
  #[validate(func = |ctx: &(), name: &str| {
    if name.len() > 5 {
      Ok(())
    } else {
      Err(Error::with_message("name_too_short", "Your name must be longer than 5 characters"))
    }
  })]
  left: String,
  #[validate(func = check)]
  right: String,
}

inner

Validates the inner fields of a slice-like type.

use wary::Wary;

#[derive(Wary)]
struct Name {
  #[validate(inner(ascii))]
  items: Vec<String>,
}

length

Validates the length of the input.

use wary::Wary;

#[derive(Wary)]
struct Name {
  // counts the length in bytes
  #[validate(length(bytes, 5..=20))]
  bytes: String,
  // counts the length in characters
  #[validate(length(chars, 5..=20))]
  chars: String,
  // counts the length in UTF-16 code units
  #[validate(length(code_units, 5..=20))]
  code_points: String,
  // counts the length in grapheme clusters
  #[validate(length(graphemes, 5..=20))]
  graphemes: String,
}

lowercase

Validates that the input is lowercase.

use wary::Wary;

#[derive(Wary)]
struct Name {
  #[validate(lowercase)]
  left: String,
  #[validate(lowercase(ascii))]
  right: String,
}

or

Meta-rule that combines multiple rules. Short-circuits on the first success.

use wary::{Wary, Validate};
use std::sync::atomic::{AtomicUsize, Ordering};

mod rule {
  pub type debug = super::DebugRule;
}

struct DebugRule;

impl DebugRule {
  fn new() -> Self {
    Self
  }
}

static DEBUG_COUNTER: AtomicUsize = AtomicUsize::new(0);

impl<I> wary::Rule<I> for DebugRule {
  type Context = ();

  fn validate(&self, _ctx: &Self::Context, item: &I) -> Result<(), wary::Error> {
    DEBUG_COUNTER.fetch_add(1, Ordering::Relaxed);
    Ok(())
  }
}

#[derive(Wary)]
struct NameOr {
  #[validate(or(equals(other = 1), custom(debug)))]
  value: u8
}

# fn main() {
let name = NameOr {
  value: 1,
};

let report = name.validate(&()).unwrap();

assert_eq!(DEBUG_COUNTER.load(Ordering::Relaxed), 0);
# }

prefix

Validates that the input starts with a substring or subslice.

use wary::Wary;

#[derive(Wary)]
struct Name(
  #[validate(prefix(str = "hello"))]
  String
);

range

Validates that the input is within a range.

use wary::Wary;

#[derive(Wary)]
struct Age {
  #[validate(range(18..=100))]
  number: u8,
  #[validate(range('a'..='z'))]
  char: char,
  #[validate(range("hello".."world"))]
  string: String,
}

regex (requires feature regex)

Validates that the input matches a regex.

use wary::Wary;

#[derive(Wary)]
struct Name(
  #[validate(regex(pat = "^[a-z]+$"))]
  String
);

required

Validates that the input is not empty. For example, that an Option is Some or a Vec is not empty.

use wary::Wary;

#[derive(Wary)]
struct Name {
  #[validate(required)]
  first: String,
  #[validate(required)]
  last: Option<String>,
}

semver (requires feature semver)

Validates that the input is a semver.

use wary::Wary;

#[derive(Wary)]
struct Version(
  #[validate(semver)]
  String
);

suffix

Validates that the input ends with a substring or subslice.

use wary::Wary;

#[derive(Wary)]
struct Name(
  #[validate(suffix(str = "hello"))]
  String
);

uppercase

Validates that the input is uppercase.

use wary::Wary;

#[derive(Wary)]
struct Name {
  #[validate(uppercase)]
  left: String,
  #[validate(uppercase(ascii))]
  right: String,
}

url (requires feature url)

Validates that the input is a url.

use wary::Wary;

#[derive(Wary)]
struct Url(
  #[validate(url)]
  String
);

uuid (requires feature uuid)

Validates that the input is a uuid.

use wary::Wary;

#[derive(Wary)]
struct Uuid(
  #[validate(uuid)]
  String
);

Implementing Validate manually

In the rare case you need to manually implement Validate, you will need to keep in mind about reporting errors properly.

use wary::{Validate, Error, error::{Path, Report}};

struct Name {
  value: String,
}

impl Validate for Name {
  type Context = ();

  fn validate_into(&self, _ctx: &Self::Context, parent: &Path, report: &mut Report) {
    if self.value.len() < 5 {
      report.push(
        parent.append("value"),
        Error::with_message("name_too_short", "Your name must be longer than 5 characters"),
      );
    }
  }
}

let name = Name {
  value: "Jane".to_string(),
};

assert!(name.validate(&()).is_err());

let longer = Name {
  value: "Jane Doe".to_string(),
};

assert!(longer.validate(&()).is_ok());

Transformation rules

Transformation rules are applied similarly to validation rules, but are implemented in the Transform trait instead.

rule trait feature dependency
custom Transformer - -
dive Transform - -
lowercase AsMut<str> (for ascii only) - -
inner AsMutSlice - -
uppercase AsMut<str> (for ascii only) - -

custom

Transforms the input with a custom Transformer.

use wary::{Wary, Transformer};

struct SecretTransformer;

impl SecretTransformer {
  fn new() -> Self {
    Self
  }
}

impl Transformer<String> for SecretTransformer {
  type Context = ();

  fn transform(&self, _ctx: &Self::Context, item: &mut String) {
    item.clear();
    item.push_str("secret");
  }
}

#[allow(non_camel_case_types)]
mod transformer {
  pub type secret = super::SecretTransformer;
}

#[derive(Wary)]
struct Person {
  #[transform(custom(secret))]
  name: String,
}

# fn main() {}

dive

Transforms the inner fields of a struct or enum.

use wary::Wary;

#[derive(Wary)]
struct Item {
  #[transform(lowercase)]
  name: String,
}

#[derive(Wary)]
struct Name {
  #[transform(dive)]
  item: Item,
}

lowercase

Transforms the input to lowercase.

use wary::Wary;

#[derive(Wary)]
struct Name {
  #[transform(lowercase)]
  left: String,
  #[transform(lowercase(ascii))]
  right: String,
}

inner

Transforms the inner fields of a slice-like type.

use wary::Wary;

#[derive(Wary)]
struct Name {
  #[transform(inner(lowercase))]
  items: Vec<String>,
}

uppercase

Transforms the input to uppercase.

use wary::Wary;

#[derive(Wary)]
struct Name {
  #[transform(uppercase)]
  left: String,
  #[transform(uppercase(ascii))]
  right: String,
}

Implementing Transform manually

use wary::Transform;

struct Name {
  value: String,
}

impl Transform for Name {
  type Context = ();

  fn transform(&mut self, _ctx: &Self::Context) {
    self.value.make_ascii_lowercase();
  }
}

let mut name = Name {
  value: "Jane".to_string(),
};

name.transform(&());

assert_eq!(name.value, "jane");

Dependencies

~0.2–2MB
~38K SLoC