A fluent validation library for Rust with a builder pattern API. FluentVal provides an intuitive, chainable API for validating data structures with comprehensive error reporting.
- 🎯 Fluent Builder API - Chain validation rules in a readable, expressive way
- 📝 Comprehensive Rules - Built-in validators for strings, numbers, emails, and more
- đź”§ Custom Rules - Define your own validation logic
- 📊 Rich Error Reporting - Detailed validation errors grouped by property
- 🚀 Type-Safe - Leverages Rust's type system for compile-time safety
Add this to your Cargo.toml:
[dependencies]
fluentval = "0.1.0"use fluentval::*;
let rule_fn = RuleBuilder::<String>::for_property("name")
.not_empty(None::<String>)
.min_length(3, None::<String>)
.max_length(50, None::<String>)
.build();
let errors = rule_fn(&"ab".to_string());
// Returns validation errors if validation failsuse fluentval::*;
#[derive(Debug)]
struct User {
name: String,
email: String,
age: i32,
}
let validator = ValidatorBuilder::<User>::new()
.rule_for("name", |u| &u.name,
RuleBuilder::for_property("name")
.not_empty(None::<String>)
.min_length(2, None::<String>))
.rule_for("email", |u| &u.email,
RuleBuilder::for_property("email")
.not_empty(None::<String>)
.email(None::<String>))
.rule_for("age", |u| &u.age,
RuleBuilder::for_property("age")
.greater_than_or_equal(18, None::<String>))
.build();
let user = User {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
age: 25,
};
let result = validate(&user, &validator);
if result.is_valid() {
println!("User is valid!");
} else {
for error in result.errors() {
println!("{}: {}", error.property, error.message);
}
}You can specify custom error messages for each rule to provide more meaningful feedback:
use fluentval::*;
#[derive(Debug)]
struct User {
name: String,
email: String,
age: i32,
password: String,
}
let validator = ValidatorBuilder::<User>::new()
.rule_for("name", |u| &u.name,
RuleBuilder::for_property("name")
.not_empty(Some("Name is required"))
.min_length(2, Some("Name must be at least 2 characters long"))
.max_length(50, Some("Name cannot exceed 50 characters")))
.rule_for("email", |u| &u.email,
RuleBuilder::for_property("email")
.not_empty(Some("Email address is required"))
.email(Some("Please provide a valid email address")))
.rule_for("age", |u| &u.age,
RuleBuilder::for_property("age")
.greater_than_or_equal(18, Some("You must be at least 18 years old"))
.less_than_or_equal(120, Some("Age must be realistic")))
.rule_for("password", |u| &u.password,
RuleBuilder::for_property("password")
.not_empty(Some("Password is required"))
.min_length(8, Some("Password must be at least 8 characters"))
.must(|p| p.chars().any(|c| c.is_ascii_uppercase()),
"Password must contain at least one uppercase letter")
.must(|p| p.chars().any(|c| c.is_ascii_digit()),
"Password must contain at least one number"))
.build();
let invalid_user = User {
name: "A".to_string(), // Too short
email: "invalid-email".to_string(), // Invalid format
age: 15, // Too young
password: "weak".to_string(), // Too short and missing requirements
};
let result = validate(&invalid_user, &validator);
if !result.is_valid() {
for error in result.errors() {
println!("{}: {}", error.property, error.message);
}
// Output:
// name: Name must be at least 2 characters long
// email: Please provide a valid email address
// age: You must be at least 18 years old
// password: Password must be at least 8 characters
// password: Password must contain at least one uppercase letter
// password: Password must contain at least one number
}not_empty()- Validates that a string is not empty or whitespacemin_length(min)- Validates minimum string lengthmax_length(max)- Validates maximum string lengthlength(min, max)- Validates string length rangeemail()- Validates email format
greater_than(min)- Value must be greater than minimumgreater_than_or_equal(min)- Value must be greater than or equal to minimumless_than(max)- Value must be less than maximumless_than_or_equal(max)- Value must be less than or equal to maximuminclusive_between(min, max)- Value must be within range (inclusive)
not_null()- Validates that an Option is Some
rule(predicate)- Add a custom validation rulemust(predicate, message)- Validate with a custom predicate
Validate a property based on other properties in the same struct. The must() method in ValidatorBuilder allows you to access both the entire object and the property value:
use fluentval::*;
#[derive(Debug)]
struct Command {
country_iso_code: String,
phone_number: String,
alt_phone_number: String,
tax_number: String,
}
// Helper function to validate phone number based on country
fn is_valid_phone_for_country(phone: &str, country_code: &str) -> bool {
match country_code {
"US" => phone.len() == 10 && phone.chars().all(|c| c.is_ascii_digit()),
"UK" => phone.len() == 11 && phone.starts_with('0'),
_ => phone.len() >= 8 && phone.len() <= 15,
}
}
// Helper function to validate tax number based on country
fn is_valid_tax_number(tax_number: &str, country_code: &str) -> bool {
match country_code {
"US" => tax_number.len() == 9 && tax_number.chars().all(|c| c.is_ascii_digit()),
"UK" => tax_number.len() == 10 && tax_number.starts_with("GB"),
_ => tax_number.len() >= 8 && tax_number.len() <= 15,
}
}
let validator = ValidatorBuilder::<Command>::new()
// Validate phone number based on country
.must("phoneNumber", |c| &c.phone_number,
|command, phone| is_valid_phone_for_country(phone, &command.country_iso_code),
"Phone number is not valid for the specified country")
// Validate that alt phone is different from primary phone
.must("altPhoneNumber", |c| &c.alt_phone_number,
|command, alt_phone| alt_phone != &command.phone_number,
"Alternative phone number must be different from primary phone number")
// Validate tax number based on country
.must("taxNumber", |c| &c.tax_number,
|command, tax_number| is_valid_tax_number(tax_number, &command.country_iso_code),
"Tax number is not valid for the specified country")
.build();
// Example: Invalid phone number for US
let invalid_command = Command {
country_iso_code: "US".to_string(),
phone_number: "123".to_string(), // Too short for US
alt_phone_number: "9876543210".to_string(),
tax_number: "123456789".to_string(),
};
let result = validate(&invalid_command, &validator);
if !result.is_valid() {
for error in result.errors() {
println!("{}: {}", error.property, error.message);
}
// Output: phoneNumber: Phone number is not valid for the specified country
}
// Example: Alt phone same as primary
let invalid_command2 = Command {
country_iso_code: "US".to_string(),
phone_number: "1234567890".to_string(),
alt_phone_number: "1234567890".to_string(), // Same as primary
tax_number: "123456789".to_string(),
};
let result = validate(&invalid_command2, &validator);
if !result.is_valid() {
for error in result.errors() {
println!("{}: {}", error.property, error.message);
}
// Output: altPhoneNumber: Alternative phone number must be different from primary phone number
}You can also validate a property without using the object context (ignore the object parameter with _):
#[derive(Debug)]
struct Registration {
country: String,
email: String,
}
// Simulate allowed countries
fn is_allowed_country(country: &str) -> bool {
vec!["US", "UK", "CA", "AU"].contains(&country)
}
let validator = ValidatorBuilder::<Registration>::new()
// Validate country without needing the object context
.must("country", |r| &r.country,
|_, country| is_allowed_country(country),
"Country is not in the allowed list")
.build();All rules accept optional custom error messages:
RuleBuilder::<String>::for_property("email")
.email(Some("Please provide a valid email address"))
.min_length(5, Some("Email must be at least 5 characters"))let result = validate(&user, &validator);
// Check if valid
if result.is_valid() {
// Handle valid case
}
// Get all errors
for error in result.errors() {
println!("{}: {}", error.property, error.message);
}
// Get errors grouped by property
let errors_by_prop = result.errors_by_property();
// Get first error for a specific property
if let Some(message) = result.first_error_for("email") {
println!("Email error: {}", message);
}MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
Contributions are welcome! Please feel free to submit a Pull Request.