check-if

バリデーションライブラリRust軽量シンプル関数型チェーン

ライブラリ

check-if

概要

check-ifは、Rustでシンプルで軽量なバリデーションを行うためのライブラリです。直感的なAPI設計により、基本的なデータ検証を効率的に実装できます。関数型プログラミングスタイルのチェーンメソッドを採用し、複雑なバリデーションロジックを読みやすく構築できる特徴があります。Rustの型安全性を活かしながら、最小限の依存関係で高速なバリデーション処理を提供する実用的なライブラリです。

詳細

check-if 0.1系は軽量性を重視して設計されたRustバリデーションライブラリで、基本的なデータ型の検証機能を提供します。文字列、数値、ブール値、配列などの一般的なバリデーションに対応し、カスタムバリデーター機能も備えています。Rustの所有権システムと型安全性を最大限に活用し、コンパイル時の型チェックとランタイムバリデーションを組み合わせて堅牢なデータ検証を実現します。メモリ効率と実行速度を重視した設計により、高パフォーマンスなアプリケーションでの使用に適しています。

主な特徴

  • 軽量かつ高速: 最小限の依存関係で高速なバリデーション処理
  • チェーンメソッド: 読みやすい関数型プログラミングスタイル
  • 型安全性: Rustの型システムを活用した安全なバリデーション
  • カスタムバリデーター: 独自のバリデーションルールの定義が可能
  • zero-cost抽象化: パフォーマンスオーバーヘッドの最小化
  • シンプルAPI: 学習コストが低く直感的な使用感

メリット・デメリット

メリット

  • Rustエコシステムでの軽量で高速なバリデーション
  • 関数型スタイルの読みやすいチェーンメソッド
  • 最小限の依存関係による軽量なバイナリサイズ
  • Rustの型安全性による信頼性の高いバリデーション
  • カスタムバリデーション機能による柔軟性
  • zero-cost抽象化によるパフォーマンス重視の設計

デメリット

  • 比較的新しいライブラリでコミュニティが小さい
  • 高度なバリデーション機能は他のライブラリより限定的
  • ドキュメントや使用例がvalidatorなどと比べて少ない
  • 複雑なバリデーションルールには向かない場合がある
  • エコシステムの成熟度が他の主要ライブラリより低い

参考ページ

書き方の例

インストールと基本セットアップ

# Cargo.toml
[dependencies]
check-if = "0.1"

# 開発時のテスト用
[dev-dependencies]
tokio = { version = "1.0", features = ["full"] }

基本的なバリデーション

use check_if::*;

fn main() {
    // 基本的な文字列バリデーション
    let name = "John Doe";
    let is_valid = check_if::string(name)
        .is_not_empty()
        .min_length(2)
        .max_length(50)
        .validate();
    
    println!("Name validation: {}", is_valid);
    
    // 数値バリデーション
    let age = 25;
    let age_valid = check_if::number(age)
        .min(0)
        .max(120)
        .validate();
    
    println!("Age validation: {}", age_valid);
    
    // メールアドレスの簡単な検証
    let email = "[email protected]";
    let email_valid = check_if::string(email)
        .is_not_empty()
        .contains("@")
        .contains(".")
        .validate();
    
    println!("Email validation: {}", email_valid);
    
    // 配列のバリデーション
    let numbers = vec![1, 2, 3, 4, 5];
    let array_valid = check_if::array(&numbers)
        .is_not_empty()
        .min_length(1)
        .max_length(10)
        .validate();
    
    println!("Array validation: {}", array_valid);
}

// 結果とエラーハンドリング
fn validate_user_data() -> Result<(), String> {
    let username = "alice123";
    let password = "secretpass";
    
    // ユーザー名の検証
    if !check_if::string(username)
        .is_not_empty()
        .min_length(3)
        .max_length(20)
        .is_alphanumeric()
        .validate() {
        return Err("無効なユーザー名です".to_string());
    }
    
    // パスワードの検証
    if !check_if::string(password)
        .is_not_empty()
        .min_length(8)
        .validate() {
        return Err("パスワードが短すぎます".to_string());
    }
    
    Ok(())
}

// バリデーション結果の詳細取得
#[derive(Debug)]
struct ValidationResult {
    is_valid: bool,
    errors: Vec<String>,
}

fn detailed_validation(input: &str) -> ValidationResult {
    let mut errors = Vec::new();
    
    if input.is_empty() {
        errors.push("入力が空です".to_string());
    }
    
    if input.len() < 3 {
        errors.push("3文字以上である必要があります".to_string());
    }
    
    if input.len() > 50 {
        errors.push("50文字以下である必要があります".to_string());
    }
    
    ValidationResult {
        is_valid: errors.is_empty(),
        errors,
    }
}

高度なバリデーションとカスタムルール

use check_if::*;
use std::collections::HashMap;

// 構造体のバリデーション
#[derive(Debug)]
struct User {
    username: String,
    email: String,
    age: u32,
    profile: UserProfile,
}

#[derive(Debug)]
struct UserProfile {
    first_name: String,
    last_name: String,
    bio: Option<String>,
}

impl User {
    fn validate(&self) -> Result<(), Vec<String>> {
        let mut errors = Vec::new();
        
        // ユーザー名のバリデーション
        if !check_if::string(&self.username)
            .is_not_empty()
            .min_length(3)
            .max_length(30)
            .validate() {
            errors.push("ユーザー名が無効です".to_string());
        }
        
        // メールアドレスのバリデーション
        if !self.is_valid_email(&self.email) {
            errors.push("メールアドレスが無効です".to_string());
        }
        
        // 年齢のバリデーション
        if !check_if::number(self.age)
            .min(13)
            .max(120)
            .validate() {
            errors.push("年齢が範囲外です".to_string());
        }
        
        // プロフィールのバリデーション
        if let Err(mut profile_errors) = self.profile.validate() {
            errors.append(&mut profile_errors);
        }
        
        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
    
    fn is_valid_email(&self, email: &str) -> bool {
        check_if::string(email)
            .is_not_empty()
            .contains("@")
            .validate() 
            && email.split('@').count() == 2
            && email.ends_with(".com") || email.ends_with(".org") || email.ends_with(".net")
    }
}

impl UserProfile {
    fn validate(&self) -> Result<(), Vec<String>> {
        let mut errors = Vec::new();
        
        // 名前のバリデーション
        if !check_if::string(&self.first_name)
            .is_not_empty()
            .min_length(1)
            .max_length(50)
            .validate() {
            errors.push("名前が無効です".to_string());
        }
        
        if !check_if::string(&self.last_name)
            .is_not_empty()
            .min_length(1)
            .max_length(50)
            .validate() {
            errors.push("苗字が無効です".to_string());
        }
        
        // バイオの検証(オプショナル)
        if let Some(bio) = &self.bio {
            if !check_if::string(bio)
                .max_length(500)
                .validate() {
                errors.push("バイオが長すぎます".to_string());
            }
        }
        
        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}

// カスタムバリデーター関数
fn validate_phone_number(phone: &str) -> bool {
    // 日本の電話番号形式の検証
    let cleaned = phone.replace("-", "").replace(" ", "");
    
    check_if::string(&cleaned)
        .is_not_empty()
        .min_length(10)
        .max_length(11)
        .validate()
        && cleaned.chars().all(|c| c.is_ascii_digit())
        && (cleaned.starts_with("090") || cleaned.starts_with("080") || cleaned.starts_with("070"))
}

fn validate_postal_code(code: &str) -> bool {
    // 日本の郵便番号形式の検証 (XXX-XXXX)
    if code.len() != 8 {
        return false;
    }
    
    let parts: Vec<&str> = code.split('-').collect();
    if parts.len() != 2 {
        return false;
    }
    
    check_if::string(parts[0])
        .length_eq(3)
        .validate()
        && check_if::string(parts[1])
            .length_eq(4)
            .validate()
        && code.chars().filter(|&c| c != '-').all(|c| c.is_ascii_digit())
}

// 複合バリデーション
struct ValidationConfig {
    required_fields: Vec<String>,
    min_lengths: HashMap<String, usize>,
    max_lengths: HashMap<String, usize>,
}

impl ValidationConfig {
    fn new() -> Self {
        let mut min_lengths = HashMap::new();
        min_lengths.insert("username".to_string(), 3);
        min_lengths.insert("password".to_string(), 8);
        min_lengths.insert("email".to_string(), 5);
        
        let mut max_lengths = HashMap::new();
        max_lengths.insert("username".to_string(), 30);
        max_lengths.insert("password".to_string(), 128);
        max_lengths.insert("email".to_string(), 254);
        
        ValidationConfig {
            required_fields: vec![
                "username".to_string(),
                "email".to_string(),
                "password".to_string(),
            ],
            min_lengths,
            max_lengths,
        }
    }
    
    fn validate_field(&self, field_name: &str, value: &str) -> Result<(), Vec<String>> {
        let mut errors = Vec::new();
        
        // 必須フィールドチェック
        if self.required_fields.contains(&field_name.to_string()) {
            if !check_if::string(value)
                .is_not_empty()
                .validate() {
                errors.push(format!("{}は必須です", field_name));
                return Err(errors);
            }
        }
        
        // 最小長チェック
        if let Some(&min_len) = self.min_lengths.get(field_name) {
            if !check_if::string(value)
                .min_length(min_len)
                .validate() {
                errors.push(format!("{}は{}文字以上である必要があります", field_name, min_len));
            }
        }
        
        // 最大長チェック
        if let Some(&max_len) = self.max_lengths.get(field_name) {
            if !check_if::string(value)
                .max_length(max_len)
                .validate() {
                errors.push(format!("{}は{}文字以下である必要があります", field_name, max_len));
            }
        }
        
        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}

// 使用例
fn main() {
    let user = User {
        username: "alice123".to_string(),
        email: "[email protected]".to_string(),
        age: 25,
        profile: UserProfile {
            first_name: "Alice".to_string(),
            last_name: "Smith".to_string(),
            bio: Some("Software engineer from Tokyo".to_string()),
        },
    };
    
    match user.validate() {
        Ok(()) => println!("ユーザーデータは有効です"),
        Err(errors) => {
            println!("バリデーションエラー:");
            for error in errors {
                println!("- {}", error);
            }
        }
    }
    
    // 電話番号の検証
    let phone = "090-1234-5678";
    if validate_phone_number(phone) {
        println!("電話番号は有効です: {}", phone);
    } else {
        println!("電話番号が無効です: {}", phone);
    }
    
    // 郵便番号の検証
    let postal = "123-4567";
    if validate_postal_code(postal) {
        println!("郵便番号は有効です: {}", postal);
    } else {
        println!("郵便番号が無効です: {}", postal);
    }
    
    // 設定ベースのバリデーション
    let config = ValidationConfig::new();
    let username = "bob";
    
    match config.validate_field("username", username) {
        Ok(()) => println!("ユーザー名は有効です"),
        Err(errors) => {
            for error in errors {
                println!("エラー: {}", error);
            }
        }
    }
}

Web フレームワーク統合とエラーハンドリング

// Actix-Web との統合例
use actix_web::{web, App, HttpResponse, HttpServer, Result as ActixResult};
use serde::{Deserialize, Serialize};
use check_if::*;

#[derive(Deserialize, Debug)]
struct CreateUserRequest {
    username: String,
    email: String,
    password: String,
    age: Option<u32>,
}

#[derive(Serialize)]
struct ValidationError {
    field: String,
    message: String,
}

#[derive(Serialize)]
struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    errors: Option<Vec<ValidationError>>,
}

impl CreateUserRequest {
    fn validate(&self) -> Result<(), Vec<ValidationError>> {
        let mut errors = Vec::new();
        
        // ユーザー名のバリデーション
        if !check_if::string(&self.username)
            .is_not_empty()
            .min_length(3)
            .max_length(30)
            .validate() {
            errors.push(ValidationError {
                field: "username".to_string(),
                message: "ユーザー名は3-30文字である必要があります".to_string(),
            });
        }
        
        // メールアドレスのバリデーション
        if !check_if::string(&self.email)
            .is_not_empty()
            .contains("@")
            .contains(".")
            .validate() {
            errors.push(ValidationError {
                field: "email".to_string(),
                message: "有効なメールアドレスを入力してください".to_string(),
            });
        }
        
        // パスワードのバリデーション
        if !check_if::string(&self.password)
            .is_not_empty()
            .min_length(8)
            .validate() {
            errors.push(ValidationError {
                field: "password".to_string(),
                message: "パスワードは8文字以上である必要があります".to_string(),
            });
        }
        
        // 年齢のバリデーション(オプショナル)
        if let Some(age) = self.age {
            if !check_if::number(age)
                .min(13)
                .max(120)
                .validate() {
                errors.push(ValidationError {
                    field: "age".to_string(),
                    message: "年齢は13-120歳の範囲で入力してください".to_string(),
                });
            }
        }
        
        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }
}

async fn create_user(req: web::Json<CreateUserRequest>) -> ActixResult<HttpResponse> {
    match req.validate() {
        Ok(()) => {
            // ユーザー作成ロジック
            println!("ユーザー作成: {:?}", req.into_inner());
            
            let response = ApiResponse {
                success: true,
                data: Some("ユーザーが正常に作成されました"),
                errors: None,
            };
            
            Ok(HttpResponse::Created().json(response))
        }
        Err(validation_errors) => {
            let response = ApiResponse::<String> {
                success: false,
                data: None,
                errors: Some(validation_errors),
            };
            
            Ok(HttpResponse::BadRequest().json(response))
        }
    }
}

// Warp との統合例(フィルター使用)
use warp::{Filter, Rejection, Reply};

#[derive(Debug)]
struct ValidationErrorWrapper(Vec<ValidationError>);
impl warp::reject::Reject for ValidationErrorWrapper {}

fn validate_create_user() -> impl Filter<Extract = (CreateUserRequest,), Error = Rejection> + Clone {
    warp::body::json()
        .and_then(|req: CreateUserRequest| async move {
            match req.validate() {
                Ok(()) => Ok(req),
                Err(errors) => Err(warp::reject::custom(ValidationErrorWrapper(errors))),
            }
        })
}

async fn handle_validation_error(err: Rejection) -> Result<impl Reply, Rejection> {
    if let Some(validation_error) = err.find::<ValidationErrorWrapper>() {
        let response = ApiResponse::<String> {
            success: false,
            data: None,
            errors: Some(validation_error.0.clone()),
        };
        
        Ok(warp::reply::with_status(
            warp::reply::json(&response),
            warp::http::StatusCode::BAD_REQUEST,
        ))
    } else {
        Err(err)
    }
}

// Axum との統合例
use axum::{
    extract::Json,
    http::StatusCode,
    response::{IntoResponse, Json as ResponseJson},
    routing::post,
    Router,
};

async fn axum_create_user(Json(req): Json<CreateUserRequest>) -> impl IntoResponse {
    match req.validate() {
        Ok(()) => {
            let response = ApiResponse {
                success: true,
                data: Some("ユーザーが正常に作成されました"),
                errors: None,
            };
            
            (StatusCode::CREATED, ResponseJson(response)).into_response()
        }
        Err(validation_errors) => {
            let response = ApiResponse::<String> {
                success: false,
                data: None,
                errors: Some(validation_errors),
            };
            
            (StatusCode::BAD_REQUEST, ResponseJson(response)).into_response()
        }
    }
}

// バリデーション結果のキャッシュとパフォーマンス最適化
use std::collections::HashMap;
use std::sync::{Arc, RwLock};

#[derive(Clone)]
struct ValidationCache {
    cache: Arc<RwLock<HashMap<String, bool>>>,
}

impl ValidationCache {
    fn new() -> Self {
        ValidationCache {
            cache: Arc::new(RwLock::new(HashMap::new())),
        }
    }
    
    fn get_or_validate<F>(&self, key: &str, validator: F) -> bool
    where
        F: FnOnce() -> bool,
    {
        // まずキャッシュから読み取り試行
        if let Ok(cache) = self.cache.read() {
            if let Some(&result) = cache.get(key) {
                return result;
            }
        }
        
        // キャッシュにない場合はバリデーション実行
        let result = validator();
        
        // 結果をキャッシュに保存
        if let Ok(mut cache) = self.cache.write() {
            cache.insert(key.to_string(), result);
        }
        
        result
    }
    
    fn clear(&self) {
        if let Ok(mut cache) = self.cache.write() {
            cache.clear();
        }
    }
}

// 高速バリデーション例
fn fast_validation_example() {
    let cache = ValidationCache::new();
    
    let email = "[email protected]";
    let cache_key = format!("email:{}", email);
    
    let is_valid = cache.get_or_validate(&cache_key, || {
        check_if::string(email)
            .is_not_empty()
            .contains("@")
            .contains(".")
            .validate()
    });
    
    println!("Email validation result: {}", is_valid);
    
    // 2回目の呼び出しはキャッシュから取得
    let is_valid_cached = cache.get_or_validate(&cache_key, || {
        panic!("This should not be called due to caching");
    });
    
    println!("Cached validation result: {}", is_valid_cached);
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("check-if バリデーションの例");
    
    // 基本的なバリデーション例
    fast_validation_example();
    
    // Webサーバーの例(コメントアウト)
    /*
    let app = Router::new()
        .route("/users", post(axum_create_user));
    
    println!("サーバーを http://localhost:3000 で起動中...");
    axum::Server::bind(&"0.0.0.0:3000".parse()?)
        .serve(app.into_make_service())
        .await?;
    */
    
    Ok(())
}

テストとベンチマーク

#[cfg(test)]
mod tests {
    use super::*;
    use check_if::*;

    #[test]
    fn test_basic_string_validation() {
        // 正常なケース
        assert!(check_if::string("valid_string")
            .is_not_empty()
            .min_length(5)
            .max_length(20)
            .validate());
        
        // 異常なケース
        assert!(!check_if::string("")
            .is_not_empty()
            .validate());
        
        assert!(!check_if::string("ab")
            .min_length(5)
            .validate());
        
        assert!(!check_if::string("this_is_a_very_long_string")
            .max_length(10)
            .validate());
    }
    
    #[test]
    fn test_number_validation() {
        // 正常なケース
        assert!(check_if::number(42)
            .min(0)
            .max(100)
            .validate());
        
        // 異常なケース
        assert!(!check_if::number(-5)
            .min(0)
            .validate());
        
        assert!(!check_if::number(150)
            .max(100)
            .validate());
    }
    
    #[test]
    fn test_user_validation() {
        let valid_user = User {
            username: "testuser".to_string(),
            email: "[email protected]".to_string(),
            age: 25,
            profile: UserProfile {
                first_name: "Test".to_string(),
                last_name: "User".to_string(),
                bio: Some("Test bio".to_string()),
            },
        };
        
        assert!(valid_user.validate().is_ok());
        
        let invalid_user = User {
            username: "".to_string(), // 空のユーザー名
            email: "invalid-email".to_string(), // 無効なメール
            age: 200, // 無効な年齢
            profile: UserProfile {
                first_name: "".to_string(), // 空の名前
                last_name: "User".to_string(),
                bio: None,
            },
        };
        
        let errors = invalid_user.validate().unwrap_err();
        assert!(!errors.is_empty());
        assert!(errors.len() >= 3); // 複数のエラーが発生
    }
    
    #[test]
    fn test_phone_number_validation() {
        // 有効な電話番号
        assert!(validate_phone_number("090-1234-5678"));
        assert!(validate_phone_number("08012345678"));
        assert!(validate_phone_number("070 1234 5678"));
        
        // 無効な電話番号
        assert!(!validate_phone_number("123-456-789")); // 短すぎる
        assert!(!validate_phone_number("050-1234-5678")); // 050で始まる
        assert!(!validate_phone_number("090-abcd-efgh")); // 文字が含まれる
    }
    
    #[test]
    fn test_postal_code_validation() {
        // 有効な郵便番号
        assert!(validate_postal_code("123-4567"));
        assert!(validate_postal_code("000-0000"));
        
        // 無効な郵便番号
        assert!(!validate_postal_code("12-3456")); // 形式不正
        assert!(!validate_postal_code("123-456")); // 短すぎる
        assert!(!validate_postal_code("1234567")); // ハイフンなし
        assert!(!validate_postal_code("abc-defg")); // 文字が含まれる
    }
    
    #[test]
    fn test_validation_config() {
        let config = ValidationConfig::new();
        
        // 有効なケース
        assert!(config.validate_field("username", "validuser").is_ok());
        assert!(config.validate_field("email", "[email protected]").is_ok());
        
        // 無効なケース
        assert!(config.validate_field("username", "ab").is_err()); // 短すぎる
        assert!(config.validate_field("email", "").is_err()); // 空
        assert!(config.validate_field("password", "short").is_err()); // 短すぎる
    }
    
    #[test]
    fn test_validation_cache() {
        let cache = ValidationCache::new();
        
        let email = "[email protected]";
        let cache_key = format!("email:{}", email);
        
        // 最初の呼び出し
        let result1 = cache.get_or_validate(&cache_key, || {
            check_if::string(email)
                .is_not_empty()
                .contains("@")
                .validate()
        });
        
        // 2回目の呼び出し(キャッシュから取得)
        let result2 = cache.get_or_validate(&cache_key, || {
            panic!("This should not be called");
        });
        
        assert_eq!(result1, result2);
        assert!(result1);
    }
}

// ベンチマークテスト
#[cfg(test)]
mod benchmarks {
    use super::*;
    use std::time::Instant;
    
    #[test]
    fn benchmark_validation_performance() {
        let iterations = 100_000;
        
        // 基本的なバリデーションのベンチマーク
        let start = Instant::now();
        for i in 0..iterations {
            let username = format!("user{}", i);
            let _ = check_if::string(&username)
                .is_not_empty()
                .min_length(3)
                .max_length(20)
                .validate();
        }
        let duration = start.elapsed();
        
        println!("基本バリデーション {} 回: {:?}", iterations, duration);
        println!("1回あたり: {:?}", duration / iterations);
        
        // 複雑なバリデーションのベンチマーク
        let start = Instant::now();
        for i in 0..iterations {
            let user = User {
                username: format!("user{}", i),
                email: format!("user{}@example.com", i),
                age: 25,
                profile: UserProfile {
                    first_name: "Test".to_string(),
                    last_name: "User".to_string(),
                    bio: Some("Test bio".to_string()),
                },
            };
            let _ = user.validate();
        }
        let duration = start.elapsed();
        
        println!("複雑バリデーション {} 回: {:?}", iterations, duration);
        println!("1回あたり: {:?}", duration / iterations);
    }
}