- PVSM.RU - https://www.pvsm.ru -

Казалось бы, валидация данных — это одна из базовых задач в программировании, которая встретится и в начале изучения языка вместе с "Hello world!", и в том или ином виде будет присутствовать в множестве зрелых проектов. Тем не менее, Google до сих пор выдает ноль релевантных результатов при попытке найти универсальную библиотеку валидации данных с открытым исходным кодом на C++.
В лучшем случае, там найдутся или инструменты проверки самого кода C++, или библиотеки валидации определенных форматов, например, таких как JSON или XML. Похоже на то, что либо разработчики для каждого случая реализуют валидацию данных вручную, либо инструменты валидации создаются под конкретный проект и плохо приспособлены для использования в качестве универсальных библиотек.
Если в комментариях кто-то сможет привести примеры открытых библиотек валидации данных на C++ помимо отдельных GUI-форм, то буду очень признателен и добавлю соответствующий список в статью.
Стоить отметить, что мотивом к разработке валидатора данных для C++ послужило не столько отсутствие подобной библиотеки, сколько желание получить инструмент, при помощи которого можно было бы единообразно описывать:
Т.е., если с прикладной точки зрения во всех трех случаях речь идет о разных представлениях одной и той же сущности, то почему бы не сделать так, чтобы правила проверки характеристик этой сущности описывались бы одинаковым образом? И уже потом эти правила обрабатывать разными способами применительно к каждому конкретному случаю — например, транслировать в строку запроса к базе данных или в проверку набора содержимого ранее заполненного объекта или в проверку отдельных переменных перед записью в объект.
В итоге, библиотека валидации разрабатывалась с учетом основного требования, чтобы было четкое разделение между:
То есть, правила валидации должны предварительно описываться в отдельном месте, желательно с применением синтаксиса, похожего на декларативный. В другом месте должны быть реализованы обработчики правил валидации. Причем, разные обработчики могут транслировать те же самые правила в разные операции — например, один обработчик использует правила для фактической проверки данных, а другой обработчик транслирует правила в запросы SQL. И, собственно, третий участок — это непосредственно применение правил валидации конкретным обработчиком в момент вызова процедуры валидации.
cpp-validator [17] является header-only библиотекой для современного C++ с поддержкой стандартов C++14/C++17. В коде cpp-validator активно используется метапрограммирование на шаблонах и библиотека Boost.Hana [18].
Основные возможности библиотеки cpp-validator перечислены ниже.
Базовая валидация данных с использованием cpp-validator выполняется в три шага:
// определение валидатора
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 [17] и готова к использованию — на момент написания статьи номер стабильной версии 1.0.2. Библиотека распространяется под лицензией Boost 1.0 [19].
Приветствуются замечания, пожелания и дополнения.
// определение валидатора
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();
Автор: Евгений
Источник [20]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/358328
Ссылки в тексте:
[1] Мотивация: #мотивация
[2] Возможности библиотеки: #возможности-библиотеки
[3] Использование: #использование-библиотеки
[4] Текущий статус: #текущий-статус-библиотеки
[5] Примеры: #примеры
[6] Тривиальная валидация числа: #тривиальная-валидация-числа
[7] Валидация с исключением: #валидация-с-исключением
[8] Явное применение валидатора к переменной: #явное-применение-валидатора-к-переменной
[9] Составной валидатор: #составной-валидатор
[10] Проверить, что число входит в интервал, и напечатать описание ошибки: #проверить-что-число-входит-в-интервал-и-напечатать-описание-ошибки
[11] Составной валидатор для проверки элемента контейнера: #составной-валидатор-для-проверки-элемента-контейнера
[12] Проверить элементы вложенных контейнеров: #проверить-элементы-вложенных-контейнеров
[13] Провести валидацию кастомного свойства объекта: #провести-валидацию-кастомного-свойства-объекта
[14] Пре-валидация данных перед записью: #пре-валидация-данных-перед-записью
[15] Один и тот же валидатор для пост-валидации и пре-валидации: #один-и-тот-же-валидатор-для-пост-валидации-и-пре-валидации
[16] Перевод ошибок валидации на русский язык: #перевод-ошибок-валидации-на-русский-язык
[17] cpp-validator: https://github.com/evgeniums/cpp-validator
[18] Boost.Hana: https://www.boost.org/doc/libs/release/libs/hana/
[19] Boost 1.0: http://boost.org/LICENSE_1_0.txt
[20] Источник: https://habr.com/ru/post/525204/?utm_source=habrahabr&utm_medium=rss&utm_campaign=525204
Нажмите здесь для печати.