Казалось бы, валидация данных — это одна из базовых задач в программировании, которая встретится и в начале изучения языка вместе с "Hello world!", и в том или ином виде будет присутствовать в множестве зрелых проектов. Тем не менее, Google до сих пор выдает ноль релевантных результатов при попытке найти универсальную библиотеку валидации данных с открытым исходным кодом на C++.


В лучшем случае, там найдутся или инструменты проверки самого кода C++, или библиотеки валидации определенных форматов, например, таких как JSON или XML. Похоже на то, что либо разработчики для каждого случая реализуют валидацию данных вручную, либо инструменты валидации создаются под конкретный проект и плохо приспособлены для использования в качестве универсальных библиотек.


Если в комментариях кто-то сможет привести примеры открытых библиотек валидации данных на C++ помимо отдельных GUI-форм, то буду очень признателен и добавлю соответствующий список в статью.


Содержание



Мотивация


Стоить отметить, что мотивом к разработке валидатора данных для C++ послужило не столько отсутствие подобной библиотеки, сколько желание получить инструмент, при помощи которого можно было бы единообразно описывать:


  • условия выборки записей в базе данных;
  • условия проверки на сервере содержимого команд, поступающих от клиентов;
  • условия проверки данных, вводимых через пользовательский интерфейс на клиенте, перед их записью в объект и отправкой на сервер.

Т.е., если с прикладной точки зрения во всех трех случаях речь идет о разных представлениях одной и той же сущности, то почему бы не сделать так, чтобы правила проверки характеристик этой сущности описывались бы одинаковым образом? И уже потом эти правила обрабатывать разными способами применительно к каждому конкретному случаю — например, транслировать в строку запроса к базе данных или в проверку набора содержимого ранее заполненного объекта или в проверку отдельных переменных перед записью в объект.


В итоге, библиотека валидации разрабатывалась с учетом основного требования, чтобы было четкое разделение между:


  • описанием правил валидации;
  • реализацией обработчиков правил валидации;
  • обработкой конкретных правил валидации конкретным обработчиком.

То есть, правила валидации должны предварительно описываться в отдельном месте, желательно с применением синтаксиса, похожего на декларативный. В другом месте должны быть реализованы обработчики правил валидации. Причем, разные обработчики могут транслировать те же самые правила в разные операции — например, один обработчик использует правила для фактической проверки данных, а другой обработчик транслирует правила в запросы SQL. И, собственно, третий участок — это непосредственно применение правил валидации конкретным обработчиком в момент вызова процедуры валидации.


Возможности библиотеки


cpp-validator является header-only библиотекой для современного C++ с поддержкой стандартов C++14/C++17. В коде cpp-validator активно используется метапрограммирование на шаблонах и библиотека Boost.Hana.


Основные возможности библиотеки cpp-validator перечислены ниже.


  • Валидация данных для различных конструкций языка:
    • простых переменных;
    • свойств объектов, включая:
      • переменные классов;
      • методы классов вида getter;
    • содержимого и свойств контейнеров;
    • иерархических типов данных, таких как вложенные объекты и контейнеры.
  • Пост-валидация объектов, когда проверяется содержимое уже заполненного объекта на соответствие сразу всем правилам.
  • Пре-валидация данных, когда перед записью в объект проверяются только те свойства, которые планируется изменить.
  • Комбинация правил с использованием логических связок AND, OR и NOT.
  • Массовая проверка элементов контейнеров с условиями ALL или ANY.
  • Частично подготовленные правила валидации с отложенной подстановкой аргументов (lazy operands).
  • Сравнение друг с другом разных свойств одного и того же объекта.
  • Автоматическая генерация описания ошибок валидации:
    • широкие возможности по настройке генерации текста ошибок;
    • перевод текста ошибок на различные языки с учетом грамматических атрибутов слов, например, числа, рода и т.д.
  • Расширяемость:
    • регистрация новых свойств объектов, доступных для валидации;
    • добавление новых операторов правил валидации;
    • добавление новых обработчиков правил валидации (адаптеров).
  • Операторы, уже встроенные в библиотеку:
    • сравнения;
    • лексикографические, с учетом и без учета регистра;
    • существования элементов;
    • проверки вхождения в интервал или набор;
    • регулярные выражения.
  • Широкая поддержка платформ и компиляторов, включая компиляторы Clang, GCC, MSVC и операционные системы Windows, Linux, macOS, iOS, Android.

Использование библиотеки


Базовая валидация данных с использованием cpp-validator выполняется в три шага:


  1. сперва создается валидатор, содержащий правила валидации, описанные с использованием почти-декларативного языка;
  2. затем валидатор применяется к объекту валидации;
  3. в конце проверяется результат валидации, для работы с которым может использоваться либо специальный объект ошибки, либо исключение.

// определение валидатора
auto container_validator=validator(
   _[size](eq,1), // размер контейнера должен быть равен 1
   _["field1"](exists,true), // поле "field1" должно существовать в контейнере
   _["field1"](ne,"undefined") // поле "field1" должно быть не равно "undefined"
);

// успешная валидация
std::map<std::string,std::string> map1={{"field1","value1"}};
validate(map1,container_validator);

// неуспешная валидация, с объектом ошибки
error_report err;
std::map<std::string,std::string> map2={{"field2","value2"}};
validate(map2,container_validator,err);
if (err)
{
    std::cerr<<err.message()<<std::endl;
    /* напечатает:
    field1 must exist
    */
}

// неуспешная валидация, с исключением
try
{
    std::map<std::string,std::string> map3={{"field1","undefined"}};
    validate(map3,container_validator);
}
catch(const validation_error& ex)
{
    std::cerr<<ex.what()<<std::endl;
    /* напечатает:
    field1 must be not equal to undefined
    */
}

При расширенном использовании cpp-validator можно регистрировать новые валидируемые свойства объектов, настраивать генерацию текстовых описаний ошибок, добавлять новые операторы или создавать полностью новые адаптеры-обработчики правил валидаций, которые, вообще говоря, могут использоваться даже не для валидации как таковой, а для выполнения других специфических задач.


Текущий статус библиотеки


Библиотека cpp-validator доступна на GitHub по адресу https://github.com/evgeniums/cpp-validator и готова к использованию — на момент написания статьи номер стабильной версии 1.0.2. Библиотека распространяется под лицензией Boost 1.0.


Приветствуются замечания, пожелания и дополнения.


Примеры


Тривиальная валидация числа


// определение валидатора
auto v=validator(gt,100); // больше чем 100

// объект ошибки
error err;

// условия не выполнены
validate(90,v,err);
if (err)
{
  // валидация неуспешна
}

// условия выполнены
validate(200,v,err);
if (!err)
{
  // валидация успешна
}

Валидация с исключением


// определение валидатора
auto v=validator(gt,100); // больше чем 100

try
{
    validate(200,v); // успешно
    validate(90,v); // генерирует исключение
}
catch (const validation_error& err)
{
    std::cerr << err.what() << std::endl;
    /* напечатает:
    must be greater than 100
    */
}

Явное применение валидатора к переменной


// определение валидатора
auto v=validator(gt,100); // больше чем 100

// применить валидатор к переменным

int value1=90;
if (!v.apply(value1))
{
  // валидация неуспешна
}

int value2=200;
if (v.apply(value2))
{
  // валидация успешна
}

Составной валидатор


// валидатор: размер меньше 15 и значение бинарно больше или равно "sample string"
auto v=validator(
  length(lt,15),
  value(gte,"sample string")
);

// явное применение валидатора к переменным

std::string str1="sample";
if (!v.apply(str1))
{
  // валидация неупешна потому что sample бинарно меньше, чем sample string
}

std::string str2="sample string+";
if (v.apply(str2))
{
  // валидация успешна
}

std::string str3="too long sample string";
if (!v.apply(str3))
{
  // валидация неуспешна, потому что длина строки больше 15 символов
}

Проверить, что число входит в интервал, и напечатать описание ошибки


// валидатор: входит в интервал [95,100]
auto v=validator(in,interval(95,100));

// объект ошибки
error_report err;

// проверить значение
size_t val=90;
validate(val,v,err);
if (err)
{
    std::cerr << err.message() << std::endl; 
    /* напечатает:
    must be in interval [95,100]
    */
}

Составной валидатор для проверки элемента контейнера


// составной валидатор
auto v=validator(
                _["field1"](gte,"xxxxxx")
                 ^OR^
                _["field1"](size(gte,100) ^OR^ value(gte,"zzzzzzzzzzzz"))
            );

// валидация контейнера и печать ошибки

error_report err;
std::map<std::string,std::string> test_map={{"field1","value1"}};
validate(test_map,v,err);
if (err)
{
    std::cerr << err.message() << std::endl;
    /* напечатает:
    field1 must be greater than or equal to xxxxxx OR size of field1 must be greater than or equal to 100 OR field1 must be greater than or equal to zzzzzzzzzzzz
    */
}

Проверить элементы вложенных контейнеров


// составной валидатор элементов вложенных контейнеров
auto v=validator(
                _["field1"][1](in,range({10,20,30,40,50})),
                _["field1"][2](lt,100),
                _["field2"](exists,false),
                _["field3"](empty(flag,true))
            );

// валидация вложенного контейнера и печать ошибки
error_report err;
std::map<std::string,std::map<size_t,size_t>> nested_map={
            {"field1",{{1,5},{2,50}}},
            {"field3",{}}
        };
validate(nested_map,v,err);
if (err)
{
    std::cerr << err.message() << std::endl;
    /* напечатает:
    element #1 of field1 must be in range [10, 20, 30, 40, 50]
    */
}

Провести валидацию кастомного свойства объекта


// структура с getter методом
struct Foo
{
    bool red_color() const
    {
        return true;
    }
};

// зарегистрировать новое свойство red_color
DRACOSHA_VALIDATOR_PROPERTY_FLAG(red_color,"Must be red","Must be not red");

// валидатор зарегистрированного свойства red_color
auto v=validator(
    _[red_color](flag,false)
);

// провести валидацию кастомного свойства и напечатать ошибку

error_report err;
Foo foo_instance;
validate(foo_instance,v,err);
if (err)
{
    std::cerr << err.message() << std::endl;
    /* напечатает:
    "Must be not red"
    */
}

Пре-валидация данных перед записью


// структура с переменными и методом вида setter
struct Foo
{
    std::string bar_value;

    uint32_t other_value;
    size_t some_size;

    void set_bar_value(std::string val)
    {
        bar_value=std::move(val);
    }
};

using namespace DRACOSHA_VALIDATOR_NAMESPACE;

// зарегистрировать кастомные свойства
DRACOSHA_VALIDATOR_PROPERTY(bar_value);
DRACOSHA_VALIDATOR_PROPERTY(other_value);

// специализация шаблона класса set_member_t для записи свойства bar_value структуры Foo
DRACOSHA_VALIDATOR_NAMESPACE_BEGIN

template <>
struct set_member_t<Foo,DRACOSHA_VALIDATOR_PROPERTY_TYPE(bar_value)>
{
    template <typename ObjectT, typename MemberT, typename ValueT>
    void operator() (
            ObjectT& obj,
            MemberT&&,
            ValueT&& val
        ) const
    {
        obj.set_bar_value(std::forward<ValueT>(val));
    }
};

DRACOSHA_VALIDATOR_NAMESPACE_END

// валидатор с кастомными свойствами
auto v=validator(
    _[bar_value](ilex_ne,"UNKNOWN"), // лексикографическое "не равно" без учета регистра
    _[other_value](gte,1000) // больше или равно 1000
);

Foo foo_instance;
error_report err;

// запись валидного значение в свойство bar_value объекта foo_instance
set_validated(foo_instance,bar_value,"Hello world",v,err);
if (!err)
{
    // свойство bar_value объекта foo_instance успешно записано
}

// попытка записи невалидного значение в свойство bar_value объекта foo_instance
set_validated(foo_instance,bar_value,"unknown",v,err);
if (err)
{
    // запись не удалась
    std::cerr << err.message() << std::endl;
    /* напечатает:
     bar_value must be not equal to UNKNOWN
     */
}

Один и тот же валидатор для пост-валидации и пре-валидации


#include <iostream>
#include <dracosha/validator/validator.hpp>
#include <dracosha/validator/validate.hpp>
using namespace DRACOSHA_VALIDATOR_NAMESPACE;

namespace validator_ns {

// зарегистрировать getter свойства "x"
DRACOSHA_VALIDATOR_PROPERTY(GetX);

// валидатор GetX
auto MyClassValidator=validator(
   /* 
   "x" в кавычках - это имя поля, которое писать в отчете вместо GetX;
   interval.open() - модификатор открытого интервала без учета граничных точек
   */
   _[GetX]("x")(in,interval(0,500,interval.open())) 
);

}
using namespace validator_ns;

// определение тестового класса  
class MyClass {
  double x;

public:

  // Конструктор с пост-валидацией
  MyClass(double _x) : x(_x) {
      validate(*this,MyClassValidator);
  }

  // Getter
  double GetX() const noexcept
  {
     return _x;
  }

  // Setter с пре-валидацией
  void SetX(double _x) {
    validate(_[validator_ns::GetX],_x,MyClassValidator);
    x = _x;
  }
};

int main()
{

// конструктор с валидным аргументом
try {
    MyClass obj1{100.0}; // ok
}
catch (const validation_error& err)
{
}

// конструктор с невалидным аргументом
try {
    MyClass obj2{1000.0}; // значение вне интервала
}
catch (const validation_error& err)
{
    std::cerr << err.what() << std::endl;
    /*
     напечатает:
     x must be in interval(0,500)
    */
}

MyClass obj3{100.0};

// запись с валидным аргументом
try {
    obj3.SetX(200.0); // ok
}
catch (const validation_error& err)
{
}

// попытка записи с невалидным аргументом
try {
    obj3.SetX(1000.0); // значение вне интервала
}
catch (const validation_error& err)
{
    std::cerr << err.what() << std::endl;
    /*
     напечатает:
     x must be in interval (0,500)
    */
}

return 0;
}

Перевод ошибок валидации на русский язык


// переводчик ключей контейнера на русский язык с учетом рода, падежа и числа
phrase_translator tr;
tr["password"]={
                    {"пароль"},
                    {"пароля",grammar_ru::roditelny_padezh}
               };
tr["hyperlink"]={
                    {{"гиперссылка",grammar_ru::zhensky_rod}},
                    {{"гиперссылки",grammar_ru::zhensky_rod},grammar_ru::roditelny_padezh}
                };
tr["words"]={
                {{"слова",grammar_ru::mn_chislo}}
            };

/* 
финальный переводчик включает в себя встроенный переводчик на русский
validator_translator_ru() и переводчик tr для имен элементов
*/
auto tr1=extend_translator(validator_translator_ru(),tr);

// контейнер для валидации
std::map<std::string,std::string> m1={
    {"password","123456"},
    {"hyperlink","zzzzzzzzz"}
};

// адаптер с генерацией отчета об ошибке на русском языке
std::string rep;
auto ra1=make_reporting_adapter(m1,make_reporter(rep,make_formatter(tr1)));

// различные валидаторы и печать ошибок на русском языке

auto v1=validator(
    _["words"](exists,true)
 );
if (!v1.apply(ra1))
{
    std::cerr<<rep<<std::endl;
    /*
    напечатает:
    слова должны существовать
    */
}
rep.clear();

auto v2=validator(
    _["hyperlink"](eq,"https://www.boost.org")
 );
if (!v2.apply(ra1))
{
    std::cerr<<rep<<std::endl;
    /*
    напечатает:
    гиперссылка должна быть равна https://www.boost.org
    */
}
rep.clear();

auto v3=validator(
    _["password"](length(gt,7))
 );
if (!v3.apply(ra1))
{
    std::cerr<<rep<<std::endl;
    /*
    напечатает:
    длина пароля должна быть больше 7
    */
}
rep.clear();

auto v4=validator(
    _["hyperlink"](length(lte,7))
 );
if (!v4.apply(ra1))
{
    std::cerr<<rep<<std::endl;
    /*
    напечатает:
    длина гиперссылки должна быть меньше или равна 7
    */
}
rep.clear();