RustのValidator Libraryのgardeを使ってみる

2023.08.10

Introduction

プログラムを書くときに必ずといっていいほど実装する
処理の1つにバリデーションがあります。
バリデーションは入力データが特定の条件を満たしているかどうか
チェックするプロセスです。
この処理はソフトウェアの安全性を保つために非常に重要です。

本稿ではRustのValidationライブラリである
garde(axumとも統合されてるヤツ)を使ってValidationを実装してみます。

Environment

  • Rust : 1.70.0

Setup

CargoでRustプロジェクトを作成し、
gardeクレートをインストールします。

% cargo new garde_example && cd garde_example
% cargo add garde

Try

gardeでは構造体やEnumにattributeを付与することで
Validationルールを定義します。

基本的な使い方

garde::Validateを使用して、構造体にValidateルールを追加します。

#[derive(Validate)]
struct User<'a> {
    #[garde(required)]
    id: Option<String>,

    #[garde(ascii, length(min=3, max=25))]
    name: &'a str,

    #[garde(length(min=20))]
    pass: &'a str,

    #[garde(email)]
    mail: &'a str,

    #[garde(skip)]  
    memo:&'a str,
}

User型変数を使ってみます。
validate関数を実行することでValidation処理が行われます。

   let user = User {
        id:Some("ID0001".to_string()),
        name: "a",
        pass: "mypassword",
        mail:"hoge",
        foo:"hello"
    };

    if let Err(e) = user.validate(&()) {
        println!("invalid user:\n{e}");
    }

mailやlengthのチェックがちゃんとされてます。

% cargo run

invalid user: 
value.mail: not a valid email: value is missing `@`
value.name: length is lower than 3
value.pass: length is lower than 20

なお、構造体だけでなくEnumを対象にすると下記のようになります。

#[derive(Validate)]
enum Data {
    Struct {
        #[garde(range(min=-1, max=10))]
        field: i32,
    },
    Tuple(
        #[garde(ascii)]
        String
    ),
}

・
・
・

   let data = Data::Struct { field: 100 };

   if let Err(e) = data.validate(&()) {
       println!("invalid data: {e}");
    }

なお、Validateionルールにrequieredが付与していた場合、
それ以外のルールはSome型だった場合に検証されます。

gardeが標準で用意している検証ルールは下記になります。

name format validation feature flag
required #[garde(required)] is value set -
ascii #[garde(ascii)] only contains ASCII -
alphanumeric #[garde(alphanumeric)] only letters and digits -
email #[garde(email)] an email according to the HTML5 spec1 email
url #[garde(url)] a URL url
ip #[garde(ip)] an IP address (either IPv4 or IPv6) -
ipv4 #[garde(ipv4)] an IPv4 address -
ipv6 #[garde(ipv6)] an IPv6 address -
credit card #[garde(credit_card)] a credit card number credit-card
phone number #[garde(phone_number)] a phone number phone-number
length #[garde(length(min=<usize>, max=<usize>))] a container with length in min..=max -
byte_length #[garde(byte_length(min=<usize>, max=<usize>))] a byte sequence with length in min..=max -
range #[garde(range(min=<expr>, max=<expr>))] a number in the range min..=max -
contains #[garde(contains(<string>))] a string-like value containing a substring -
prefix #[garde(prefix(<string>))] a string-like value prefixed by some string -
suffix #[garde(suffix(<string>))] a string-like value suffixed by some string -
pattern #[garde(pattern("<regex>"))] a string-like value matching some regex regex
pattern #[garde(pattern(<matcher>))] a string-like value matched by some Matcher -
dive #[garde(dive)] nested validation, calls validate on the value -
skip #[garde(skip)] skip validation -
custom #[garde(custom(<function or closure>))] a custom validator -

カスタムバリデーション

バリデーションは自由にカスタマイズすることができます。
指定したフィールドも文字列が任意のVec<String>に含まれるか
チェックするValidationを定義してみます。

#[derive(garde::Validate)]
#[garde(context(MapContext))]
struct MyLang {
    #[garde(custom(is_langs))]
    lang: String,
}

struct MapContext {
    langs: Vec<String>,
}

fn is_langs(value: &str, context: &MapContext) -> garde::Result {
    if context.langs.contains(&value.to_string()) {
        println!("The value is present in the Vec!");
    } else {
        println!("The value is not present in the Vec.");
        return Err(garde::Error::new(format!("{} {}", value, "is not present in the Vec.")));
    }
    Ok(())
}

validateにContextを渡し、自作のValidateion関数(is_langs)でチェックします。

let ctx = MapContext {
    langs: vec![
        "rust".to_string(),
        "java".to_string(),
        "javascript".to_string(),
    ],
};
let mylang = MyLang { 
    lang : "java".to_string()
};
if let Err(e) = mylang.validate(&ctx){
    println!("invalid data: {e}");
}

Implementing Validate

ネストされたバリデーションをサポートしたいコンテナ型がある場合、
#[garde(dive)]を使ってValidationを実装できます。

#[repr(transparent)]
#[derive(Debug)]
struct MyVec<T>(Vec<T>);

impl<T: garde::Validate + std::fmt::Debug> garde::Validate for MyVec<T> {
    type Context = T::Context;

    fn validate(&self, ctx: &Self::Context) -> Result<(), garde::Errors> {
        garde::Errors::list(|errors| {
            for item in self.0.iter() {
                errors.push(item.validate(ctx));
            }
        })
        .finish()
    }
}

#[derive(Debug,garde::Validate)]
struct Foo {
  #[garde(dive)]
  field: MyVec<Bar>,
}

#[derive(Debug,garde::Validate)]
struct Bar {
  #[garde(range(min = 1, max = 10))]
  value: u32,
}

Summary

今回はRustのValidateionライブラリgardeを使ってみました。
シンプルでカスタマイズも簡単なのですぐに使えると思います。

References