cpp-validator

バリデーションライブラリC++ヘッダーオンリー変数検証オブジェクト検証コンテナ検証

ライブラリ

cpp-validator

概要

cpp-validatorは、C++14/17対応のモダンなヘッダーオンリーバリデーションライブラリです。変数、オブジェクト、コンテナの検証に特化しており、シンプルで読みやすいAPIを提供します。事前検証と事後検証の両方をサポートし、自動的なエラーメッセージ生成機能とロケール対応により、国際化されたアプリケーション開発に適しています。Clang、GCC、MSVCで動作確認済みで、現代的なC++プロジェクトに簡単に統合できます。

詳細

cpp-validatorは、データ制約を宣言的に定義し、必要に応じて適用するためのライブラリです。バリデーションルールを特定のコードポイントで定義し、他の場所でオンデマンドで適用できる柔軟な設計となっています。エラーメッセージはユーザーのロケールを考慮して自動生成され、複雑な条件付きバリデーションや入れ子のオブジェクト検証も支援します。例外ベースとエラーレポートベースの両方のエラーハンドリングパターンを提供し、開発者の好みに応じて使い分けることができます。

主な特徴

  • ヘッダーオンリー: 簡単な統合と配布
  • 宣言的API: 読みやすく保守しやすいバリデーション記述
  • 多様な検証対象: 変数、オブジェクト、コンテナの包括的な検証
  • 自動エラーメッセージ: ロケール対応の自動メッセージ生成
  • 柔軟なエラーハンドリング: 例外ベースとレポートベース
  • モダンC++: C++14/17機能を活用した設計

メリット・デメリット

メリット

  • ヘッダーオンリーライブラリで導入が簡単
  • シンプルで直感的なAPIデザイン
  • 自動的なエラーメッセージ生成とロケール対応
  • 事前検証と事後検証の両方をサポート
  • 入れ子のオブジェクトや複雑な条件付きバリデーション対応
  • 豊富なカスタマイズオプション

デメリット

  • 比較的新しいライブラリで採用実績が少ない
  • 大規模なライブラリエコシステムとの統合例が限定的
  • 高度なスキーマバリデーション機能は提供しない
  • パフォーマンスクリティカルな用途での最適化が不明確
  • ドキュメントが他の主要ライブラリと比較して少ない

参考ページ

書き方の例

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

# Conanを使用してインストール
conan install cpp-validator/1.0@evgeniums/stable

# vcpkgを使用してインストール
vcpkg install cpp-validator

# 手動でヘッダーファイルをダウンロード
wget https://github.com/evgeniums/cpp-validator/archive/refs/heads/master.zip
unzip master.zip
# include/を適切なディレクトリに配置

基本的な変数とオブジェクトの検証

#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <cpp-validator/adapters/reporting.hpp>
#include <iostream>
#include <string>

namespace cv = cpp_validator;

int main() {
    // 基本的な数値バリデーション
    {
        // 100より大きい値のバリデーター
        auto v = cv::validator(cv::gt, 100);
        
        int value1 = 90;
        if (!v.apply(value1)) {
            std::cout << "値 " << value1 << " は100より大きくありません" << std::endl;
        }
        
        int value2 = 150;
        if (v.apply(value2)) {
            std::cout << "値 " << value2 << " は有効です" << std::endl;
        }
    }
    
    // 文字列バリデーション
    {
        // 文字列の値と長さのバリデーター
        auto v = cv::validator(
            cv::value(cv::gte, "sample string"),  // 辞書順で "sample string" 以上
            cv::size(cv::lt, 15)                  // 文字数が15未満
        );
        
        std::string str1 = "sample";
        if (!v.apply(str1)) {
            std::cout << "文字列 \"" << str1 << "\" は条件を満たしません" << std::endl;
        }
        
        std::string str2 = "sample string+";
        if (v.apply(str2)) {
            std::cout << "文字列 \"" << str2 << "\" は有効です" << std::endl;
        }
        
        std::string str3 = "too long sample string";
        if (!v.apply(str3)) {
            std::cout << "文字列 \"" << str3 << "\" は長すぎます" << std::endl;
        }
    }
    
    // 範囲チェック
    {
        auto v = cv::validator(cv::in, cv::interval(95, 100));
        
        size_t val = 90;
        if (!v.apply(val)) {
            std::cout << "値 " << val << " は95-100の範囲にありません" << std::endl;
        }
        
        size_t val2 = 97;
        if (v.apply(val2)) {
            std::cout << "値 " << val2 << " は有効な範囲内です" << std::endl;
        }
    }
    
    return 0;
}

エラーレポートとメッセージ生成

#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <cpp-validator/adapters/reporting.hpp>
#include <iostream>
#include <string>

namespace cv = cpp_validator;

int main() {
    // エラーレポートを使用したバリデーション
    {
        auto v = cv::validator(cv::in, cv::interval(95, 100));
        
        cv::error_report err;
        size_t val = 90;
        cv::validate(val, v, err);
        
        if (err) {
            std::cout << "エラー: " << err.message() << std::endl;
            // 出力: "must be in interval [95,100]"
        }
    }
    
    // 複数条件のバリデーション
    {
        auto v = cv::validator(
            cv::value(cv::ne, "UNKNOWN"),     // "UNKNOWN"と等しくない
            cv::size(cv::lte, 32)             // 長さが32以下
        );
        
        cv::error_report err;
        std::string val = "UNKNOWN";
        cv::validate(val, v, err);
        
        if (err) {
            std::cout << "文字列バリデーションエラー: " << err.message() << std::endl;
        }
    }
    
    // 数値の詳細なバリデーション
    {
        auto v = cv::validator(
            cv::gte(0),                       // 0以上
            cv::lte(1000),                    // 1000以下
            cv::ne(500)                       // 500と等しくない
        );
        
        cv::error_report err;
        int val = 500;
        cv::validate(val, v, err);
        
        if (err) {
            std::cout << "数値バリデーションエラー: " << err.message() << std::endl;
        }
    }
    
    // カスタムエラーメッセージの設定
    {
        auto v = cv::validator(
            cv::gt(0, "値は正の数である必要があります"),
            cv::lt(100, "値は100未満である必要があります")
        );
        
        cv::error_report err;
        int val = -5;
        cv::validate(val, v, err);
        
        if (err) {
            std::cout << "カスタムエラー: " << err.message() << std::endl;
        }
    }
    
    return 0;
}

例外ベースバリデーションと複雑なオブジェクト検証

#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <cpp-validator/adapters/reporting.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <map>

namespace cv = cpp_validator;

// 複雑なデータ構造の例
struct User {
    std::string name;
    int age;
    std::string email;
    std::vector<std::string> tags;
    std::map<std::string, std::string> metadata;
};

int main() {
    // 例外ベースバリデーション
    {
        auto v = cv::validator(cv::gt, 18);
        
        try {
            cv::validate(25, v);    // 成功
            std::cout << "年齢バリデーション成功" << std::endl;
            
            cv::validate(15, v);    // 例外発生
        } catch (const cv::validation_error& err) {
            std::cout << "年齢バリデーションエラー: " << err.what() << std::endl;
        }
    }
    
    // 入れ子のオブジェクトバリデーション
    {
        // ユーザー情報のバリデーションルール
        auto userValidator = cv::validator(
            // 名前の検証
            cv::_(cv::key("name"))(
                cv::value(cv::ne, ""),               // 空文字でない
                cv::size(cv::gte, 2),                // 2文字以上
                cv::size(cv::lte, 50)                // 50文字以下
            ),
            // 年齢の検証
            cv::_(cv::key("age"))(
                cv::gte(0),                          // 0以上
                cv::lte(120)                         // 120以下
            ),
            // メールアドレスの検証
            cv::_(cv::key("email"))(
                cv::size(cv::gte, 5),                // 5文字以上
                cv::contains("@")                     // @を含む
            )
        );
        
        // テスト用のユーザーデータ
        std::map<std::string, std::string> user_data = {
            {"name", "John Doe"},
            {"age", "25"},
            {"email", "[email protected]"}
        };
        
        try {
            cv::validate(user_data, userValidator);
            std::cout << "ユーザーデータバリデーション成功" << std::endl;
        } catch (const cv::validation_error& err) {
            std::cout << "ユーザーデータバリデーションエラー: " << err.what() << std::endl;
        }
    }
    
    // コンテナバリデーション
    {
        // 数値のベクター用バリデーター
        auto vectorValidator = cv::validator(
            cv::size(cv::gte, 1),                    // 少なくとも1つの要素
            cv::size(cv::lte, 10),                   // 最大10個の要素
            cv::each(cv::gte(0))                     // 各要素が0以上
        );
        
        std::vector<int> numbers = {1, 2, 3, 4, 5};
        
        try {
            cv::validate(numbers, vectorValidator);
            std::cout << "数値配列バリデーション成功" << std::endl;
        } catch (const cv::validation_error& err) {
            std::cout << "数値配列バリデーションエラー: " << err.what() << std::endl;
        }
        
        // 無効なデータでテスト
        std::vector<int> invalid_numbers = {1, -2, 3};
        
        try {
            cv::validate(invalid_numbers, vectorValidator);
        } catch (const cv::validation_error& err) {
            std::cout << "無効な数値配列: " << err.what() << std::endl;
        }
    }
    
    return 0;
}

高度なバリデーション機能とカスタマイズ

#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <cpp-validator/adapters/reporting.hpp>
#include <iostream>
#include <string>
#include <regex>
#include <functional>

namespace cv = cpp_validator;

// カスタムバリデーション関数
bool isValidEmail(const std::string& email) {
    static const std::regex email_pattern(
        R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)"
    );
    return std::regex_match(email, email_pattern);
}

bool isValidPhoneNumber(const std::string& phone) {
    static const std::regex phone_pattern(
        R"(^[\+]?[1-9][\d]{0,15}$)"
    );
    return std::regex_match(phone, phone_pattern);
}

// 複雑なデータ構造
struct ContactInfo {
    std::string name;
    std::string email;
    std::string phone;
    int age;
    std::string country;
    bool newsletter_subscription;
};

// カスタムバリデーター クラス
class ContactValidator {
private:
    std::vector<std::string> valid_countries = {
        "Japan", "USA", "UK", "Germany", "France", "Canada"
    };
    
public:
    bool validateCountry(const std::string& country) {
        return std::find(valid_countries.begin(), valid_countries.end(), country) 
               != valid_countries.end();
    }
    
    cv::error_report validateContact(const ContactInfo& contact) {
        cv::error_report report;
        
        // 名前のバリデーション
        auto nameValidator = cv::validator(
            cv::size(cv::gte, 2, "名前は2文字以上である必要があります"),
            cv::size(cv::lte, 50, "名前は50文字以下である必要があります")
        );
        
        cv::validate(contact.name, nameValidator, report);
        
        // メールアドレスのバリデーション
        if (!isValidEmail(contact.email)) {
            report.add_error("無効なメールアドレス形式です");
        }
        
        // 電話番号のバリデーション
        if (!contact.phone.empty() && !isValidPhoneNumber(contact.phone)) {
            report.add_error("無効な電話番号形式です");
        }
        
        // 年齢のバリデーション
        auto ageValidator = cv::validator(
            cv::gte(13, "年齢は13歳以上である必要があります"),
            cv::lte(120, "年齢は120歳以下である必要があります")
        );
        
        cv::validate(contact.age, ageValidator, report);
        
        // 国のバリデーション
        if (!validateCountry(contact.country)) {
            report.add_error("サポートされていない国です");
        }
        
        return report;
    }
    
    // 条件付きバリデーション
    cv::error_report validateContactWithConditions(const ContactInfo& contact) {
        cv::error_report report;
        
        // 基本バリデーション
        auto basic_report = validateContact(contact);
        if (basic_report.has_errors()) {
            report.merge(basic_report);
        }
        
        // 条件付きバリデーション
        if (contact.newsletter_subscription) {
            // ニュースレター購読者にはメールアドレスが必須
            if (contact.email.empty()) {
                report.add_error("ニュースレター購読にはメールアドレスが必要です");
            }
        }
        
        // 未成年者の場合は特別な制限
        if (contact.age < 18) {
            if (contact.country != "Japan") {
                report.add_error("未成年者は日本在住者のみ登録可能です");
            }
        }
        
        return report;
    }
};

// 一括バリデーション
class BatchValidator {
public:
    struct ValidationResult {
        bool is_valid;
        std::vector<std::string> errors;
        size_t valid_count;
        size_t invalid_count;
        
        ValidationResult() : is_valid(true), valid_count(0), invalid_count(0) {}
    };
    
    ValidationResult validateContactList(const std::vector<ContactInfo>& contacts) {
        ValidationResult result;
        ContactValidator validator;
        
        for (size_t i = 0; i < contacts.size(); ++i) {
            const auto& contact = contacts[i];
            
            auto report = validator.validateContactWithConditions(contact);
            
            if (report.has_errors()) {
                result.invalid_count++;
                result.is_valid = false;
                
                for (const auto& error : report.errors()) {
                    result.errors.push_back(
                        "連絡先[" + std::to_string(i) + "] " + contact.name + 
                        ": " + error
                    );
                }
            } else {
                result.valid_count++;
            }
        }
        
        return result;
    }
};

int main() {
    // 単体の連絡先バリデーション
    {
        ContactInfo contact = {
            "John Doe",
            "[email protected]",
            "+1234567890",
            25,
            "USA",
            true
        };
        
        ContactValidator validator;
        auto report = validator.validateContactWithConditions(contact);
        
        if (report.has_errors()) {
            std::cout << "連絡先バリデーションエラー:" << std::endl;
            for (const auto& error : report.errors()) {
                std::cout << "  - " << error << std::endl;
            }
        } else {
            std::cout << "連絡先バリデーション成功: " << contact.name << std::endl;
        }
    }
    
    // 複数の連絡先の一括バリデーション
    {
        std::vector<ContactInfo> contacts = {
            {"Alice Smith", "[email protected]", "+1987654321", 30, "Canada", false},
            {"Bob Johnson", "invalid-email", "+1555123456", 16, "USA", true},
            {"Charlie Brown", "[email protected]", "", 25, "Germany", true},
            {"", "[email protected]", "+1444555666", 22, "InvalidCountry", false}
        };
        
        BatchValidator batchValidator;
        auto result = batchValidator.validateContactList(contacts);
        
        std::cout << "\n一括バリデーション結果:" << std::endl;
        std::cout << "有効な連絡先: " << result.valid_count << std::endl;
        std::cout << "無効な連絡先: " << result.invalid_count << std::endl;
        
        if (!result.is_valid) {
            std::cout << "エラー詳細:" << std::endl;
            for (const auto& error : result.errors) {
                std::cout << "  - " << error << std::endl;
            }
        }
    }
    
    return 0;
}

パフォーマンスとスケーラビリティ

#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <iostream>
#include <chrono>
#include <vector>
#include <random>
#include <string>

namespace cv = cpp_validator;

// パフォーマンステスト用のデータ生成
class TestDataGenerator {
private:
    std::random_device rd;
    std::mt19937 gen{rd()};
    std::uniform_int_distribution<> age_dist{18, 80};
    std::uniform_int_distribution<> name_len_dist{5, 20};
    std::uniform_int_distribution<> char_dist{'a', 'z'};
    
public:
    struct TestRecord {
        std::string name;
        int age;
        double score;
        std::vector<int> values;
    };
    
    TestRecord generateRecord() {
        TestRecord record;
        
        // ランダムな名前を生成
        int name_len = name_len_dist(gen);
        record.name.reserve(name_len);
        for (int i = 0; i < name_len; ++i) {
            record.name += static_cast<char>(char_dist(gen));
        }
        
        record.age = age_dist(gen);
        record.score = std::uniform_real_distribution<>{0.0, 100.0}(gen);
        
        // ランダムな値の配列を生成
        int value_count = std::uniform_int_distribution<>{1, 10}(gen);
        record.values.reserve(value_count);
        for (int i = 0; i < value_count; ++i) {
            record.values.push_back(std::uniform_int_distribution<>{0, 1000}(gen));
        }
        
        return record;
    }
    
    std::vector<TestRecord> generateRecords(size_t count) {
        std::vector<TestRecord> records;
        records.reserve(count);
        
        for (size_t i = 0; i < count; ++i) {
            records.push_back(generateRecord());
        }
        
        return records;
    }
};

// 高速バリデーション クラス
class FastValidator {
private:
    // 事前コンパイル済みバリデーター
    static inline const auto name_validator = cv::validator(
        cv::size(cv::gte, 2),
        cv::size(cv::lte, 50)
    );
    
    static inline const auto age_validator = cv::validator(
        cv::gte(18),
        cv::lte(120)
    );
    
    static inline const auto score_validator = cv::validator(
        cv::gte(0.0),
        cv::lte(100.0)
    );
    
    static inline const auto values_validator = cv::validator(
        cv::size(cv::gte, 1),
        cv::size(cv::lte, 20),
        cv::each(cv::gte(0))
    );
    
public:
    bool validateRecord(const TestDataGenerator::TestRecord& record) {
        return name_validator.apply(record.name) &&
               age_validator.apply(record.age) &&
               score_validator.apply(record.score) &&
               values_validator.apply(record.values);
    }
    
    struct ValidationStats {
        size_t total_records;
        size_t valid_records;
        size_t invalid_records;
        double validation_time_ms;
        double records_per_second;
    };
    
    ValidationStats validateRecords(const std::vector<TestDataGenerator::TestRecord>& records) {
        ValidationStats stats;
        stats.total_records = records.size();
        stats.valid_records = 0;
        stats.invalid_records = 0;
        
        auto start = std::chrono::high_resolution_clock::now();
        
        for (const auto& record : records) {
            if (validateRecord(record)) {
                stats.valid_records++;
            } else {
                stats.invalid_records++;
            }
        }
        
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        
        stats.validation_time_ms = duration.count() / 1000.0;
        stats.records_per_second = (stats.total_records * 1000000.0) / duration.count();
        
        return stats;
    }
};

int main() {
    // パフォーマンステストの実行
    {
        TestDataGenerator generator;
        FastValidator validator;
        
        // 小規模テスト
        std::cout << "小規模テスト (1,000レコード):" << std::endl;
        auto small_records = generator.generateRecords(1000);
        auto small_stats = validator.validateRecords(small_records);
        
        std::cout << "  総レコード数: " << small_stats.total_records << std::endl;
        std::cout << "  有効レコード数: " << small_stats.valid_records << std::endl;
        std::cout << "  無効レコード数: " << small_stats.invalid_records << std::endl;
        std::cout << "  処理時間: " << small_stats.validation_time_ms << " ms" << std::endl;
        std::cout << "  処理速度: " << small_stats.records_per_second << " records/sec" << std::endl;
        
        // 大規模テスト
        std::cout << "\n大規模テスト (100,000レコード):" << std::endl;
        auto large_records = generator.generateRecords(100000);
        auto large_stats = validator.validateRecords(large_records);
        
        std::cout << "  総レコード数: " << large_stats.total_records << std::endl;
        std::cout << "  有効レコード数: " << large_stats.valid_records << std::endl;
        std::cout << "  無効レコード数: " << large_stats.invalid_records << std::endl;
        std::cout << "  処理時間: " << large_stats.validation_time_ms << " ms" << std::endl;
        std::cout << "  処理速度: " << large_stats.records_per_second << " records/sec" << std::endl;
        
        // メモリ使用量の最適化例
        std::cout << "\nメモリ効率的な処理 (ストリーミング):" << std::endl;
        
        auto start = std::chrono::high_resolution_clock::now();
        size_t batch_size = 1000;
        size_t total_processed = 0;
        size_t total_valid = 0;
        
        for (size_t i = 0; i < 10; ++i) {
            auto batch = generator.generateRecords(batch_size);
            auto stats = validator.validateRecords(batch);
            
            total_processed += stats.total_records;
            total_valid += stats.valid_records;
            
            // バッチ処理後にメモリを解放
            batch.clear();
            batch.shrink_to_fit();
        }
        
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        
        std::cout << "  総処理レコード数: " << total_processed << std::endl;
        std::cout << "  有効レコード数: " << total_valid << std::endl;
        std::cout << "  総処理時間: " << duration.count() / 1000.0 << " ms" << std::endl;
        std::cout << "  平均処理速度: " << (total_processed * 1000000.0) / duration.count() << " records/sec" << std::endl;
    }
    
    return 0;
}