garde

Rust用の型安全なデータ検証ライブラリ。deriveマクロを使用した宣言的な検証ルールの定義が可能

概要

gardeは、Rustで型安全なデータ検証を提供する強力なライブラリです。deriveマクロを使用して、構造体やenumに対して宣言的に検証ルールを定義できます。コンパイル時の型チェックと実行時の検証を組み合わせることで、安全で表現力豊かな検証システムを実現しています。

主な特徴

  • Deriveマクロベース: #[derive(Validate)]で簡単に検証を追加
  • 豊富な組み込みバリデータ: 長さ、範囲、パターンマッチングなど
  • カスタムバリデータ: 独自の検証ロジックを簡単に実装
  • 詳細なエラー情報: フィールドごとの検証エラーを取得
  • Serde統合: JSONやその他の形式のデータ検証に対応
  • ゼロコスト抽象化: 実行時のオーバーヘッドを最小限に

インストール

[dependencies]
garde = "0.18"

# Serde統合を使用する場合
garde = { version = "0.18", features = ["serde"] }

# 正規表現バリデータを使用する場合
garde = { version = "0.18", features = ["regex"] }

# Email検証を使用する場合
garde = { version = "0.18", features = ["email"] }

# URL検証を使用する場合
garde = { version = "0.18", features = ["url"] }

基本的な使い方

シンプルな検証

use garde::Validate;

#[derive(Debug, Validate)]
struct User {
    #[garde(length(min = 3, max = 20))]
    username: String,
    
    #[garde(email)]
    email: String,
    
    #[garde(range(min = 18, max = 150))]
    age: u8,
}

fn main() {
    let user = User {
        username: "john_doe".to_string(),
        email: "[email protected]".to_string(),
        age: 25,
    };
    
    match user.validate() {
        Ok(()) => println!("検証成功!"),
        Err(e) => println!("検証エラー: {}", e),
    }
}

複数の検証ルール

use garde::Validate;

#[derive(Debug, Validate)]
struct Product {
    #[garde(length(min = 1, max = 100))]
    #[garde(ascii)]
    name: String,
    
    #[garde(range(min = 0.01))]
    price: f64,
    
    #[garde(length(max = 500))]
    #[garde(optional)]
    description: Option<String>,
    
    #[garde(contains("category-"))]
    category_id: String,
}

組み込みバリデータ

文字列検証

#[derive(Validate)]
struct StringValidation {
    // 長さ検証
    #[garde(length(min = 5, max = 50))]
    title: String,
    
    // パターンマッチング
    #[garde(pattern(r"^[a-zA-Z0-9_]+$"))]
    identifier: String,
    
    // 前後の空白を除去
    #[garde(trim)]
    name: String,
    
    // ASCII文字のみ
    #[garde(ascii)]
    ascii_only: String,
    
    // 英数字のみ
    #[garde(alphanumeric)]
    code: String,
    
    // プレフィックス検証
    #[garde(prefix("user_"))]
    user_id: String,
    
    // サフィックス検証
    #[garde(suffix(".com"))]
    domain: String,
    
    // 含む文字列検証
    #[garde(contains("@"))]
    email_like: String,
}

数値検証

#[derive(Validate)]
struct NumberValidation {
    // 範囲検証
    #[garde(range(min = 0, max = 100))]
    percentage: u8,
    
    // 最小値のみ
    #[garde(range(min = 0.0))]
    positive_amount: f64,
    
    // 最大値のみ
    #[garde(range(max = 1000))]
    limited_quantity: i32,
}

コレクション検証

#[derive(Validate)]
struct CollectionValidation {
    // 配列の長さ検証
    #[garde(length(min = 1, max = 10))]
    tags: Vec<String>,
    
    // 要素ごとの検証
    #[garde(inner(length(min = 3, max = 20)))]
    usernames: Vec<String>,
    
    // HashMapの検証
    #[garde(length(min = 1))]
    #[garde(inner(value(range(min = 0))))]
    scores: std::collections::HashMap<String, i32>,
}

カスタムバリデータ

関数ベースのカスタムバリデータ

use garde::{Validate, ValidationError};

fn validate_password(password: &str) -> Result<(), ValidationError> {
    if password.len() < 8 {
        return Err(ValidationError::new("パスワードは8文字以上必要です"));
    }
    
    let has_uppercase = password.chars().any(|c| c.is_uppercase());
    let has_lowercase = password.chars().any(|c| c.is_lowercase());
    let has_digit = password.chars().any(|c| c.is_numeric());
    
    if !has_uppercase || !has_lowercase || !has_digit {
        return Err(ValidationError::new(
            "パスワードは大文字、小文字、数字を含む必要があります"
        ));
    }
    
    Ok(())
}

#[derive(Validate)]
struct Account {
    #[garde(custom(validate_password))]
    password: String,
}

クロージャを使用したカスタム検証

#[derive(Validate)]
struct Order {
    #[garde(custom(|value: &f64| {
        if *value <= 0.0 {
            return Err(ValidationError::new("金額は0より大きい必要があります"));
        }
        if (*value * 100.0).fract() != 0.0 {
            return Err(ValidationError::new("小数点以下は2桁までです"));
        }
        Ok(())
    }))]
    amount: f64,
}

条件付き検証

#[derive(Validate)]
struct ConditionalValidation {
    is_premium: bool,
    
    // is_premiumがtrueの場合のみ検証
    #[garde(length(min = 10), if = "is_premium")]
    premium_code: Option<String>,
    
    // カスタム条件
    #[garde(range(min = 100), if = |obj: &Self| obj.is_premium)]
    credit_limit: u32,
}

ネストした構造体の検証

#[derive(Validate)]
struct Address {
    #[garde(length(min = 1, max = 100))]
    street: String,
    
    #[garde(length(min = 1, max = 50))]
    city: String,
    
    #[garde(pattern(r"^\d{3}-\d{4}$"))]
    postal_code: String,
}

#[derive(Validate)]
struct Customer {
    #[garde(length(min = 1, max = 50))]
    name: String,
    
    #[garde(email)]
    email: String,
    
    // ネストした構造体の検証
    #[garde(nested)]
    address: Address,
    
    // オプショナルなネスト
    #[garde(nested)]
    billing_address: Option<Address>,
}

エラーハンドリング

use garde::{Validate, Report};

fn validate_user_input(user: User) -> Result<(), String> {
    match user.validate() {
        Ok(()) => Ok(()),
        Err(report) => {
            // エラーレポートの詳細を取得
            let errors = format_validation_errors(&report);
            Err(errors)
        }
    }
}

fn format_validation_errors(report: &Report) -> String {
    let mut errors = Vec::new();
    
    for (field, field_errors) in report.iter() {
        for error in field_errors {
            errors.push(format!("{}: {}", field, error.message()));
        }
    }
    
    errors.join(", ")
}

Serde統合

use garde::Validate;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Validate)]
struct ApiRequest {
    #[garde(length(min = 1, max = 100))]
    title: String,
    
    #[garde(range(min = 1))]
    count: u32,
    
    #[garde(email)]
    contact_email: String,
}

// JSON APIでの使用例
fn handle_api_request(json_str: &str) -> Result<ApiRequest, String> {
    // JSONをデシリアライズ
    let request: ApiRequest = serde_json::from_str(json_str)
        .map_err(|e| format!("JSONパースエラー: {}", e))?;
    
    // 検証
    request.validate()
        .map_err(|e| format!("検証エラー: {}", e))?;
    
    Ok(request)
}

高度な使用例

複雑なビジネスルール

use garde::{Validate, ValidationError};
use chrono::{NaiveDate, Utc};

#[derive(Validate)]
struct EventRegistration {
    #[garde(length(min = 1, max = 100))]
    event_name: String,
    
    #[garde(custom(validate_future_date))]
    event_date: NaiveDate,
    
    #[garde(range(min = 1, max = 1000))]
    attendees: u32,
    
    #[garde(custom(validate_registration))]
    registration: RegistrationDetails,
}

#[derive(Clone)]
struct RegistrationDetails {
    early_bird: bool,
    discount_code: Option<String>,
    total_price: f64,
}

fn validate_future_date(date: &NaiveDate) -> Result<(), ValidationError> {
    let today = Utc::now().naive_utc().date();
    if *date <= today {
        return Err(ValidationError::new("イベント日は未来の日付である必要があります"));
    }
    Ok(())
}

fn validate_registration(reg: &RegistrationDetails) -> Result<(), ValidationError> {
    if reg.early_bird && reg.discount_code.is_some() {
        return Err(ValidationError::new(
            "早期割引とクーポンコードは併用できません"
        ));
    }
    
    if reg.total_price < 0.0 {
        return Err(ValidationError::new("価格は0以上である必要があります"));
    }
    
    Ok(())
}

バリデーションコンテキスト

use garde::{Validate, ValidationContext};

#[derive(Validate)]
#[garde(context = ValidationConfig)]
struct ConfigurableValidation {
    #[garde(custom(validate_with_config))]
    value: String,
}

struct ValidationConfig {
    min_length: usize,
    allowed_prefixes: Vec<String>,
}

fn validate_with_config(
    value: &str,
    context: &ValidationConfig
) -> Result<(), ValidationError> {
    if value.len() < context.min_length {
        return Err(ValidationError::new(
            format!("最小{}文字必要です", context.min_length)
        ));
    }
    
    let has_valid_prefix = context.allowed_prefixes.iter()
        .any(|prefix| value.starts_with(prefix));
    
    if !has_valid_prefix {
        return Err(ValidationError::new("許可されたプレフィックスで始まる必要があります"));
    }
    
    Ok(())
}

パフォーマンス最適化

use garde::Validate;
use once_cell::sync::Lazy;
use regex::Regex;

// 正規表現をキャッシュ
static PHONE_REGEX: Lazy<Regex> = Lazy::new(|| {
    Regex::new(r"^\+?[1-9]\d{1,14}$").unwrap()
});

#[derive(Validate)]
struct OptimizedValidation {
    // キャッシュされた正規表現を使用
    #[garde(custom(validate_phone))]
    phone: String,
}

fn validate_phone(phone: &str) -> Result<(), ValidationError> {
    if !PHONE_REGEX.is_match(phone) {
        return Err(ValidationError::new("無効な電話番号形式です"));
    }
    Ok(())
}

まとめ

gardeは、Rustにおける型安全で表現力豊かなデータ検証を実現する優れたライブラリです。deriveマクロによる宣言的な検証定義、豊富な組み込みバリデータ、柔軟なカスタム検証機能により、複雑なビジネスルールも簡潔に実装できます。Serde統合により、Web APIやデータ処理パイプラインでの使用も容易です。