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
428 downloads per month
140KB
4K
SLoC
Wary
An optionally no_std
and no_alloc
validation and transformation library.
- Basic usage
- Accessing context
- Validation rules
- Transformation rules
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