garde

repository·main·Indexed 21 days ago

https://github.com/jprochazk/garde

A Rust validation library that provides a declarative way to define constraints on data structures using procedural macros. It features the `Validate` derive macro, a `Report` system for aggregating errors with precise `Path` locations, and support for internationalization (i18n) of error messages. Garde includes built-in rules for emails, IP addresses, and byte/character length validation, as well as `Unvalidated<T>` and `Valid<T>` wrapper types to manage data state.

Tokens
8K
Snippets
30
Records
41
Agent score
73%

What's inside garde

  1. Email validation features: IDNA support

    main
    If the email-idna feature is enabled, the email validator supports Internationalized Domain Names in Applications (IDNA). It will attempt to convert the domain to ASCII using idna::domain_to_ascii_cow before performing validation. If the feature is disabled, IDNA domains will result in an InvalidDomain error.
  2. How simple length validation works

    main

    Simple length validation is powered by the Simple trait. The library provides implementations for several common types that have a measurable length:

    • Byte-based length (measured in bytes):

      • std::string::String and &str
      • std::borrow::Cow<'a, str>
      • std::rc::Rc<str>, std::sync::Arc<str>, std::boxed::Box<str>
    • Element-based length (measured in number of elements):

      • Vec<T> and &[T]
      • Box<[T]>
      • Fixed-size arrays [T; N] and references &[T; N]
      • std::collections::HashMap<K, V, S>
      • std::collections::HashSet<T, S>
      • std::collections::BTreeMap<K, V>
      • std::collections::BTreeSet<T>
      • std::collections::VecDeque<T>
      • std::collections::BinaryHeap<T>
      • std::collections::LinkedList<T>

    Handling Options: If you call validate_length on an Option<T>, the validation only applies if the value is Some(v). If the value is None, the validation automatically passes (Ok(())).

  3. How to customize validation error messages with I18n

    main

    By default, garde uses English error messages via the DefaultI18n implementation. To use custom error messages (e.g., for different languages), you must implement the I18n trait and wrap your validation calls in with_i18n.

    with_i18n installs the handler for the current thread only. It uses a stack guard to ensure that the previous handler is restored when the closure finishes, making it safe for nested calls.

    use garde::{Validate, i18n::{I18n, with_i18n}};
    use std::borrow::Cow;
    use std::fmt::Display;
    
    struct Czech;
    
    impl I18n for Czech {
        fn length_lower_than(&self, min: usize) -> Cow<'static, str> {
            format!("musí obsahovat alespoň {min} znaků").into()
        }
    
        fn email_invalid(&self, error: &dyn Display) -> Cow<'static, str> {
            format!("email je neplatný: {error}").into()
        }
        // Implement other required methods...
    }
    
    #[derive(Validate)]
    struct User {
        #[garde(length(min = 3))]
        name: String,
        #[garde(email)]
        email: String,
    }
    
    let user = User { name: "Jan".into(), email: "invalid".into() };
    
    // Validate using the Czech handler
    let result = with_i18n(Czech, || user.validate());
  4. Use the typestate pattern with Unvalidated and Valid

    main

    Garde uses the typestate pattern to ensure that a value has been successfully validated before it is used in your application.

    1. Wrap your raw data in Unvalidated<T>.
    2. Call .validate() or .validate_with(ctx) on the Unvalidated<T> instance.
    3. If successful, you receive a Valid<T> wrapper.

    The only way to obtain a Valid<T> is through the Unvalidated::validate methods, providing a type-level guarantee that the data has passed validation rules.

    use garde::{Unvalidated, Valid};
    
    // 1. Wrap raw data
    let unvalidated = Unvalidated::new(my_data);
    
    // 2. Validate
    match unvalidated.validate() {
        Ok(valid_data) => {
            // 3. Use Valid<T>
            let data = valid_data.into_inner();
            // ...
        }
        Err(report) => {
            // Handle errors
        }
    }
  5. How pattern matching works in Garde

    main

    Pattern validation relies on two main abstractions:

    1. Matcher: A trait for types that can check if a string matches a pattern. regex::Regex implements Matcher (when the regex feature is enabled). Matcher is also implemented for std::sync::LazyLock<T> and once_cell::sync::Lazy<T> where T: Matcher.
    2. Pattern: A trait implemented for the types being validated (the "haystack"). This trait allows the field to be checked against a Matcher.

    When validate is called, the Pattern implementation calls is_match on the provided Matcher using the field's value as the input string.

  6. Use the pattern validation rule

    main

    The #[garde(pattern(...))] rule allows you to validate a field against a pattern. You can provide a regular expression as a string literal (requires the regex feature) or an expression that implements the Matcher trait.

    Important Performance Note: The expression provided to pattern(...) is evaluated every time validate is called. To avoid expensive re-computation (like re-parsing a regex), use std::sync::LazyLock or once_cell::sync::Lazy to wrap your matcher.

    #[derive(garde::Validate)]
    struct Test {
        #[garde(pattern(r"[a-zA-Z0-9][a-zA-Z0-9_]+"))]
        v: String,
    }
  7. Optimize pattern validation with Lazy types

    main

    To prevent expensive pattern re-evaluation during every validation call, wrap your Matcher (such as a regex::Regex) in a lazy initialization type like std::sync::LazyLock or once_cell::sync::Lazy. This ensures the pattern is compiled only once.

    use std::sync::LazyLock;
    use regex::Regex;
    
    static LAZY_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[a-zA-Z0-9][a-zA-Z0-9_]+").unwrap());
    
    #[derive(garde::Validate)]
    struct Test {
        #[garde(pattern(LAZY_RE))]
        v: String,
    }
  8. Validate IP addresses with #[garde(ip)]

    main

    You can validate IP addresses in your structs using the #[garde(ip)] attribute. This rule works on any type that implements the Ip trait. By default, it has a blanket implementation for any type that implements garde::rules::AsStr (such as String or &str).

    #[derive(garde::Validate)]
    struct Test {
        #[garde(ip)]
        v: String,
    }
  9. Validate email addresses with `#[garde(email)]`

    main

    You can validate email addresses in your structs by using the #[garde(email)] attribute on a field. This works for any type that implements the Email trait, which includes all types implementing garde::rules::AsStr (like String or &str) and Option<T> where T implements Email.

    #[derive(garde::Validate)]
    struct Test {
        #[garde(email)]
        v: String,
    }
  10. Implement custom byte length validation

    main

    If you have a custom type and want to use Garde's byte length validation rules, implement the HasBytes trait for your type. This allows your type to be used with the Bytes validation logic.

    use garde::rules::length::bytes::HasBytes;
    
    struct MyData([u8; 10]);
    
    impl HasBytes for MyData {
        fn num_bytes(&self) -> usize {
            self.0.len()
        }
    }
  11. Create and inspect validation errors with `Error`

    main

    The Error struct represents a single validation failure message.

    • Use Error::new(message) to create a new error. The message can be any type that implements ToCompactString (like &str or String).
    • Use .message() to retrieve the error message as a &str.
    let err = Error::new("must be at least 5 characters long");
    println!("Error: {}", err.message());
  12. Implement the I18n trait

    main

    To provide custom error messages, implement the I18n trait. You must provide implementations for all the following rule-related methods. Most methods accept parameters like min/max values, pattern strings, or specific error reason enums (like InvalidEmail or InvalidUrl).

    impl I18n for MyLanguage {
        fn length_lower_than(&self, min: usize) -> Cow<'static, str>;
        fn length_greater_than(&self, max: usize) -> Cow<'static, str>;
        fn range_lower_than(&self, min: &dyn Display) -> Cow<'static, str>;
        fn range_greater_than(&self, max: &dyn Display) -> Cow<'static, str>;
        fn credit_card_invalid(&self, reason: InvalidCreditCard) -> Cow<'static, str>;
        fn pattern_no_match(&self, pattern: &dyn Display) -> Cow<'static, str>;
        fn contains_missing(&self, pattern: &dyn Display) -> Cow<'static, str>;
        fn url_invalid(&self, reason: InvalidUrl) -> Cow<'static, str>;
        fn prefix_missing(&self, pattern: &dyn Display) -> Cow<'static, str>;
        fn suffix_missing(&self, pattern: &dyn Display) -> Cow<'static, str>;
        fn phone_number_invalid(&self, reason: InvalidPhoneNumber) -> Cow<'static, str>;
        fn ip_invalid(&self, kind: IpKind) -> Cow<'static, str>;
        fn matches_field_mismatch(&self, field: &dyn Display) -> Cow<'static, str>;
        fn email_invalid(&self, reason: InvalidEmail) -> Cow<'static, str>;
        fn ascii_invalid(&self) -> Cow<'static, str>;
        fn alphanumeric_invalid(&self) -> Cow<'static, str>;
        fn required_not_set(&self) -> Cow<'static, str>;
    }