check-if
ライブラリ
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);
}
}