Каждый программист неоднократно сталкивался с необходимостью проверки пользовательского ввода. Занимаясь веб-разработкой уже более 10 лет, я перепробовал массу библиотек, но так и не нашел той единственной, которая решала бы поставленные мною задачи.

Основные проблемы, которые встречаются в библиотеках валидации данных

Проблема №1. Многие валидаторы проверяют только те данные, для которых описаны правила проверки. Для меня важно, чтобы любой пользовательский ввод, который явно не разрешен, был проигнорирован. То есть, валидатор должен вырезать все данные для которых не описаны правила валидации. Это просто фундаментально требование.

Проблема №2. Процедурное описание правил валидации. Я не хочу каждый раз думать про алгоритм валидации, я просто хочу описать декларативно, как должны выглядеть правильные данные. По сути, я хочу задать схему данных (почему не «JSON Schema» — в конце поста).

Проблема №3. Описание правил валидации в виде кода. Казалось бы, это не так страшно, но это сразу сводит на нет все попытки сериализации правил валидации и использования одних и тех же правил валидации на бекенде и фронтенде.

Проблема №4. Валидация останавливается на первом же поле с ошибкой. Такой подход не дает возможности подсветить сразу все ошибочные/обязательные поля в форме.

Проблема №5. Нестандартизированные сообщения об ошибках. Например, «Field name is required». Такую ошибку я не могу показать пользователю по ряду причин:
  • поле в интерфейсе может называться совсем по другому
  • интерфейс может быть не на английском
  • нужно различать тип ошибки. Например, ошибки на пустое значение показывать специальным образом

То есть, нужно возвращать не сообщение об ошибках, а стандартизированные коды ошибок.

Проблема №6. Числовые коды ошибок. Это просто неудобно в использовании. Я хочу, чтобы коды ошибок были интуитивно понятны. Согласитесь, что код ошибки «REQUIRED» понятней, чем код «27». Логика аналогична работе с классами исключений.

Проблема №7. Нет возможности проверять иерархические структуры данных. Сегодня, во времена разных JSON API, без этого просто не обойтись. Кроме самой валидации иерархических данных, нужно предусмотреть и возврат кодов ошибок для каждого поля.

Проблема №8. Ограниченный набор правил. Стандартных правил всегда не хватает. Валидатор должен быть расширяемый и позволять добавлять в него правила любой сложности.

Проблема №9. Слишком широкая сфера ответственности. Валидатор не должен генерировать формы, не должен генерировать код, не должен делать ничего, кроме валидации.

Проблема №10. Невозможность провести дополнительную обработку данных. Практически всегда, где есть валидация, есть необходимость в какой-то дополнительной (часто предварительной) обработке данных: вырезать запрещенные символы, привести в нижний регистр, удалить лишние пробелы. Особенно актуально — это удаление пробелов в начале и в конце строки. В 99% случаев они там не нужны. Я знаю, что я до этого говорил, что валидатор не должен делать ничего кроме валидации.

3 года назад, было решено написать валидатор, который не будет иметь всех вышеописанных проблем. Так появился LIVR (Language Independent Validation Rules). Есть реализации на Perl, PHP, JavaScript, Python (мы на python не пишем — фидбек по ней дать не могу). Валидатор используется в продакшене уже несколько лет практически в каждом проекте компании. Валидатор работает, как на сервере, так и на клиенте. Поиграться с валидатором можно тут — webbylab.github.io/livr-playground.

Ключевой идеей было то, что ядро валидатора должно быть минимальным, а вся логика валидации находится в правилах (вернее в их реализации). То есть, для валидатора нет разницы между правилами «required» (проверяет наличие значения), «max_length» (проверяет максимальную длину), «to_lc» (приводит данные в нижний регистра), «list_of_objects» (помогает описать правила для поля, которое содержит массив объектов).

Другими словами, валидатор ничего не знает ничего:
  • о кодах ошибок
  • о том, что он умеет валидировать иерархические объекты
  • о том, что он умеет преобразовывать/чистить данные
  • о многом другом

Все это ответственность правил валидации.

Спецификация LIVR

Поскольку задача стояла сделать валидатор независимым от языка программирования, этакий себе mustache/handlebars, но только в мире валидации данных, то начали мы с написания спецификации.

Цели спецификации:
  1. Стандартизировать формат описания данных.
  2. Описать минимальный набор правил валидации, которые должны поддерживаться каждой реализацией.
  3. Стандартизировать коды ошибок.
  4. Быть единой базовой документацией для всех реализаций.
  5. Иметь набор тестовых данных, которые позволяет проверить реализацию на соответствие спецификации

Спецификация доступна по адресу livr-spec.org

Основной идеей было то, что описание правил валидации должно выглядеть, как схема данных и максимально быть похожими на данные, только вместо значений правила.

Пример описания правил валидации для формы авторизации (демо):
{
    email: ['required', 'email'],
    password: 'required'
}

Пример правил валидации для регистрационной формы (демо):
{
    name: 'required',
    email: ['required', 'email'],
    gender: { one_of: ['male', 'female'] },
    phone: {max_length: 10},
    password: ['required', {min_length: 10} ]
    password2: { equal_to_field: 'password' }
}

Пример валидации вложенного объекта (демо):
{
    name: 'required',
    phone: {max_length: 10},
    address: { 'nested_object': {
        city: 'required',
        zip: ['required', 'positive_integer']
    }}
}

Правила валидации

Как описываются правила валиции? Каждое правило состоит из имени и аргументов (практически, как вызов функции) и в общем случае описывается следующим образом {«RULE_NAME»: ARRAY_OF_ARGUMENTS}. Для каждого поля описывается массив правил, которые применяются в порядке следования.

Например,
{
    "login": [ { length_between: [ 5, 10 ] } ]
}

То есть, у нас есть поле «login» и правило «length_between», которое имеет 2 аргумента ( «5» и «10» ). Это наиболее полная форма, но разрешены следующие упрощения

  • Если правило к полю одно, то массив не обязателен
  • Если у правила один аргумент, то можно передавать только его (не обрамляя в массив)
  • Если у правила не аргументов, то можно записать просто название правила.

Все 3 записи идентичны:
"login": [ { required: [] } ]

"login": [ "required" ]

"login": "required"

Более детально расписано в спецификации в разделе «How it works».

Поддерживаемые правила

Все правила можно разделить на 3 глобальных группы:
  • Правила, которые валидируют данные (числа, строки и тд). Например, «max_length».
  • Правила, которые позволяют составлять более сложные правила с более простых. Например, «nested_object».
  • Правила, которые преобразовывают данные. Например, «to_lc»

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

Вот общий список правил, которые должны поддерживаться каждой реализаций валидатора:

Базовые правила
  • required — поле обязательно и значение должно быть не пустым
  • not_empty — поле не обязательно, но если оно есть, то не может быть пустым
  • not_empty_list — значение должно содержать не пустой массив

Правила для проверки строк
  • one_of
  • max_length
  • min_length
  • length_between
  • length_equal
  • like

Правила для проверки чисел
  • integer
  • positive_integer
  • decimal
  • positive_decimal
  • max_number
  • min_number
  • number_between

Правила для специальных форматов
  • email
  • url
  • iso_date
  • equal_to_field

Правила для описания более сложных правил (метаправила)
  • nested_object — описывает правила для вложенного объекта
  • list_of — описывает правила, которым должен соответствовать каждый элемент списка
  • list_of_objects — значение должно быть массивом объектов нужном формате
  • list_of_different_objects — используйте, когда нужно проверить массив объектов разных типов.

Правила для преобразования данных (названия начинаются с глагола)
  • trim — убирает пробелы в начале в конце
  • to_lc — приводит к нижнему регистру
  • to_uc — приводит к верхнему регистру
  • remove — удаляет указанные символы
  • leave_only — оставляет только указанные символы

Метаправила

Пример и коды ошибок для каждого правила можно найти в LIVR-спецификации. Немного детальней остановимся лишь на метаправилах. Метаправила — это правила, которые позволяет скомбинировать простые правила в более сложные для валидации сложных иерархических структур данных. Важно понимать, что валидатор не делает различия между простыми правилами и метаправилами. Метаправила ничем не отличаются от того же “required” (да, я повторяюсь).

nested_object
Позволяет описывать правила валидации для вложенных объектов. Этим правилом вы будете пользоваться постоянно.
Код ошибки зависит от вложенных правил. Если вложенный объект не является хешом (словарем), то поле будет содержать ошибку: “FORMAT_ERROR”.
Пример использования (демо):

address: { 'nested_object': {
    city: 'required',
    zip: ['required', 'positive_integer']
}}

list_of
Позволяет описать правила валидации для списка значений. Каждое правило будет применяться для каждого элемента списка.
Код ошибки зависит от вложенных правил.
Пример использования (демо):

{ product_ids: { 'list_of': [ 'required',  'positive_integer'] }}

list_of_objects
Позволяет описать правила валидации для массива хешей(словарей). Похоже на «nested_object», но ожидает массив объектов. Правила применяются для каждого элемента в массиве.
Код ошибки зависит от вложенных правил. В случае если значение не является массивом, для поля будет возвращен код “FORMAT_ERROR”.
Пример использования (демо):
products: ['required', { 'list_of_objects': {
    product_id: ['required','positive_integer'],
    quantity: ['required', 'positive_integer']
}}]

list_of_different_objects
Аналогичен «list_of_objects», но бывает, что массив, который нам приходит, содержит объекты разного типа. Тип объекта мы можем определить по какому-то полю, например, «type». «list_of_different_objects» позволяет описать правила для списка объектов разного вида.
Код ошибки зависит от вложенных правил валидации. Если вложенных объект не является хешом, то поле будет содержать ошибку “FORMAT_ERROR”.
Пример использования (демо):

{
    products: ['required', { 'list_of_different_objects': [
        product_type, {
            material: {
                product_type: 'required',
                material_id: ['required', 'positive_integer'],
                quantity: ['required', {'min_number': 1} ],
                warehouse_id: 'positive_integer'
            },
            service: {
                product_type: 'required',
                name: ['required', {'max_length': 20} ]
            }
        }
    ]}]
}

В этом примере валидатор будут смотреть на “product_type” в каждом хеше и, в завимости от значения этого поля, будет использовать соответствующие правила валидации.

Формат ошибок

Как уже было сказано, правила возвращают строковые коды ошибок, которые понятны разработчику, например, «REQUIRED», «WRONG_EMAIL», «WRONG_DATE» и тд. Теперь разработчик может понять, в чем ошибка, осталось удобно донести в каких полях она возникла. Для этого валидатор возвращает структуру аналогичную переданной ему на валидацию, но она содержит только поля в которых возникли ошибки и вместо исходных значений в полях строковые коды ошибок.

Например, есть правила:
{
    name: 'required',
    phone: {max_length: 10},
    address: { 'nested_object': {
        city: 'required',
        zip: ['required', 'positive_integer']
    }}
}

и данные для валидации:
{
    phone: 12345678901,
    address: {
       city: 'NYC' 
    }
}

на выходе получим следующую ошибку
{
    "name": "REQUIRED",
    "phone": "TOO_LONG",
    "address": {
        "zip": "REQUIRED"
    }
}

демо валидации

REST API и формат ошибок

Возврат вменяемых ошибок всегда требует дополнительных усилий от разработчиков. И очень мало REST API, которые дают детальную информацию в ошибках. Часто это просто «Bad request» и все. Хочется, чтобы глядя на ошибку, к какому полю она относится и просто пути поля недостаточно, поскольку данные могут быть иерархическими и содержать массивы объектов… У нас в компании мы поступаем следующим образом — абсолютно для каждого запроса описываем правила валидации при помощи LIVR. В случае ошибки валидации, мы возвращаем объект ошибки клиенту. Объект ошибки содержит глобальный код ошибки и ошибку полученную от LIVR валидатора.

Например, вы передаете данные на сервер:

{
    "email": "user_at_mail_com",
    "age": 10,
    "address": {
        "country": "USQ"
    }
}

и в ответ получаете (демо валидации на livr playground):

{"error": {
    "code": "FORMAT_ERROR",
    "fields": {
        "email": "WRONG_EMAIL",
        "age": "TOO_LOW",
        "fname": "REQUIRED",
        "lname": "REQUIRED",
        "address":  {
            "country": "NOT_ALLOWED_VALUE",
            "city": "REQUIRED",
            "zip": "REQUIRED"
        }
    }
}}

Это значительно информативнее, чем какой-то «Bad request».

Работа с псевдонимами и регистрация собственных правил

Спецификация содержит только наиболее используемые правила, но у каждого проекта своя специфика и постоянно возникают ситуации, когда каких-то правил не хватает. В связи с этим, одним из ключевых требований к валидатору была возможность его расширения собственными правилами любого типа. Изначально каждая реализация имела свой механизм описания правил, но начиная со спецификации версии 0.4 мы ввели стандартный способ создания правил на базе других правил (создание псевдонимов), это покрывает 70% ситуаций. Рассмотрим оба варианта.

Создание псевдонима
Способ, каким регистрируется псевдоним зависит от реализации, но то как псевдоним описывается — регламентировано спецификацией. Такой подход, например, позволяет сериализировать описания псевдонимов и использовать их с разными реализациями (например, на Perl-бекенде и JavaScript-фронтенде)

// Регистрация псевдонима "valid_address"
validator. registerAliasedRule({
    name: 'valid_address',
    rules: { nested_object: {
        country: 'required',
        city: 'required',
        zip: 'positive_integer'
    }}
});

// Регистрация псевдонима "adult_age"
validator.registerAliasedRule( {
    name: 'adult_age',
    rules: [ 'positive_integer', { min_number: 18 } ]
});

// Теперь псевдонимы доступны, как обычные правила.
{
    name: 'required',
    age: ['required', 'adult_age' ],
    address: ['required', 'valid_address']
}


Более того, можно устанавливать свои коды ошибок для правил.

Например,
validator.registerAliasedRule({
    name: 'valid_address',
    rules: { nested_object: {
        country: 'required',
        city: 'required',
        zip: 'positive_integer'
    }},
    error: 'WRONG_ADDRESS'
});

и в случае ошибки при валидации адреса, мы получим следующее:
{
    address: 'WRONG_ADDRESS'
}

Регистрация полноценного правила на примере JavaScript реализации
Для валидации используются функции обратного вызова, которые осуществляют проверку значений. Попробуем описать новое правило под названием “strong_password”. Будем проверять, что значение больше 8 символов и содержит цифры и буквы в верхнем и нижнем регистрах.

var LIVR = require('livr');

var rules = {password: ['required', 'strong_password']};

var validator = new LIVR.Validator(rules);

validator.registerRules({
    strong_password: function() {
        return function(val) {
            // пропускаем пустые значение. Для проверки на обязательность у нас и так есть правило "required"
            if (val === undefined || val === null || val === '' ) return;
            
            if ( length(val) < 8 || !val.match([0-9]) || !val.match([a-z] || !val.match([A-Z] ) ) {
                return 'WEAK_PASSWORD';
            }

            return;
          }
    }
});

Теперь добавим возможность задавать минимальное количество символов в пароле и зарегистрируем это правило как глобальное (доступное во всех экземплярах валидатора).

var LIVR = require('livr');

var rules = {password: ['required', {'strong_password': 10}]};

var validator = new LIVR.Validator(rules);

var strongPassword = function(minLength) {
    if (!minLength) throw "[minLength] parameter required";

    return function(val) {
        // пропускаем пустые значение. Для проверки на обязательность у нас и так есть правило "required"
        if (val === undefined || val === null || val === '' ) return;
            
        if ( length(val) < minLength || !val.match([0-9]) || !val.match([a-z] || !val.match([A-Z] ) ) {
            return 'WEAK_PASSWORD';
        }

        return;
    }
};

LIVR.Validator.registerDefaultRules({ strong_password: strongPassword });


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

Есть возможность регистрации правил, которые будут не только валидировать значение, но и изменять его. Например, приводить к верхнему регистру или удалять лишние пробелы.

Своя реализация по спецификации

Если есть желание сделать свою реализацию валидатора, то для облегчения задачи был создан набор тест-кейсов . Если ваша реализация проходит все тесты, то ее можно считать корректной. Комплект тестов состоит из 4-х групп:

  • «positive» — позитивные тесты для основных правил
  • «negative» — негативные тесты для основных правил
  • «aliases_positive» — позитивные тесты для псевдонимов правил
  • «aliases_negative» — негативные тесты для псевдонимов правил

По сути, каждый тест содержит несколько файлов:

  • rules.json — описание правил валидации
  • input.json — структура, которая передается валидатору на проверку
  • output.json — очищенная структура, которая получается после валидации

Каждый негативный тест вместо «output.json» содержит «errors.json» с описанием ошибки, которая должна возникнуть в результате валидации. В тестах псевдонимов есть файл «aliases.json» с псевдонимами, которые необходимо предварительно зарегистрировать.

Почему не JSON Schema?

Часто задаваемый вопрос. Если коротко, то причин несколько:
  • Сложный формат для правил. Хочется, чтобы структура с правилами была максимально близка к структуре с данными. Попробуйте описать этот пример на JSON Schema
  • Формат ошибок никак не специфицирован и разные реализации возвращают ошибки в разном формате.
  • Нет преобразования данных, например «to_lc».

JSON Schema содержит и интересные вещи, как-то возможность задать максимальное количество элементов в списке, но в LIVR это реализуется просто добавлением еще одного правила.

Ссылки по LIVR

Комментарии (46)


  1. dos
    14.06.2015 21:08
    +2

    Как альтернативу, подпадающие под все ваши вводные правила, могу порекомендовать Joi. Из плюсов — правил валидации намного больше, ну и чуть больше возможностей, например не увидел у вас, возможно ли задавать дефолтные значения переменных, если во входящий данных пусто и можно ли у вас задавать условия ветвления, если например я хочу допустить, что в одной переменной может быть строка или массив строк и соответственно задать разные правила для каждого из случаев.


    1. koorchik Автор
      14.06.2015 21:34
      +5

      Спасибо, Joi выглядит неплохо, но ему присущи проблемы других валидаторов:
      1) Он только под JS. Нам нужно кроссплатформенное решение
      2) Правила валидации описываются в виде кода, а не структуры данных (Проблема №3)
      3) Останавливается на первой ошибке (Проблема №4)
      4) Не нашел механизма для регистрации собственных правил
      5) Сложно описывать иерархические правила (если это возможно)
      6) так далее… )

      Относительно большего количества правил, то мы старались в спеку LIVR включать необходимый минимум. LIVR легко расширяется и можно делать отдельные библиотеки правил.

      Дефолтовые значения переменных — это хорошая идея. В LIVR очень легко реализуема путем добавления правила «default». И можно будет описывать

      { age: [ 'positive_integer', {default: 18} ] }
      


      Если будет востребовано, то можно добавить правило и на уровень спеки


      1. dos
        14.06.2015 22:31
        +1

        1) Ну да, это только JS. У вас есть php и python это круто. Раньше, когда на этих языках писал, то испытывал те же проблемы что и вы — не было нормальной библиотеки для валидации. Теперь, видимо, есть, спасибо :)
        2) Можно описать в виде структуры (json), потом скомпилировать структуру в схему Joi.
        3) Есть опция, которая позволяет отобразить все ошибки, а не остановится только на первой.
        4) Если вы имеете ввиду, что для валидации данных необходимо использовать свою функцию, то для этого есть Joi.func
        5) Можно валидировать неограниченное количество вложенных объектов.


      1. dos
        14.06.2015 22:57

        Кстати, вот наверно чего не хватает всем библиотекам — это конвертирующих функций, которые исполняются после успешной валидации. В Joi они примитивны, например там в число преобразовать или в строку. Не хватает чего-нить типа такого:

        convert: function (value) {
        return value.toString().split('\n');
        }


  1. slonopotamus
    14.06.2015 21:51
    +2

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


    1. koorchik Автор
      14.06.2015 21:56

      Вначале мы перепробовали разные валидаторы, долго с ними боролись, писали обвязки всякие. А затем появился LIVR и мы перестали бороться, и начали получать удовольствие :)


      1. jonic
        14.06.2015 22:05

        Отличное решение, возьму на заметку и скорее всего буду использовать совместно с бэк/фронт частью (в этом плане вообще отлично получилось, что одни правила работают на разных языках/плфтформах и их стоит прописать один раз). Однозначно +


    1. SDSWanderer
      14.06.2015 21:57
      +6

      Выглядит так, как будто вы не читали статью.


    1. dos
      14.06.2015 23:01

      У нас, например, есть сканер, который собирает разную информацию. Сегодня в этом поле может быть число, завтра там строка, послезавтра там уже массив строк и т.п. То есть заранее не известно, что мы получим. При этом нам нужно обязательно хранить эту информацию, не важно какой бы она была. Понимаю, что это чисто наша спецефическая проблема, но всё же — заранее схему описать нельзя. Можно лишь попытаться подогнать пришедшие данные под допустимые схемы и сохранить их БД для последующей обработки.


      1. koorchik Автор
        15.06.2015 07:44
        +1

        Можно создать свое правило, которое будет смотреть на тип пришедших данных и решать, как его проверять. Если же глядя на тип данных нельзя решить, как их валидировать, то значит тут никакой валидатор не поможет :).

        Немного схожий кейс — github.com/koorchik/LIVR/issues/9


  1. egor_nullptr
    14.06.2015 22:04
    +3

    И всё же «валидатор не должен делать ничего кроме валидации» (принцип единственной ответственности).
    Пример, показывающий зависимость результата валидации от порядка следования правил, «хорошие валидаторы» так не делают.


    1. jonic
      14.06.2015 22:08

      Я бесспорно понял что Вы хотели сказать, но вы же сами задали правила валидации в таком порядке? И одно правило можно указать несколько раз… Я бы наверно даже остался в таком варианте и доверил регуляцию этих правил вышестоящему инструменту(защита от дурака)


    1. koorchik Автор
      14.06.2015 22:11
      +3

      На это мы пошли умышлено. Такой подход дает возможность организовать pipe. Вот пример.

      Кроме того, это делает валидатор более предсказуемым, поскольку есть приоритет правил. На практике такой подход показал себя очень удобным.


  1. DmitryKoterov
    14.06.2015 23:22
    +1

    Автору LIVR: посмотрите библиотеку dklab.ru/lib/HTML_MetaForm
    Эта библиотека, конечно, не блещет новизной, да и способ реализации в ней сомнителен (не говоря уж о публичном интерфейсе класса), однако я не для того ссылку на нее привожу, чтобы сравнивать, а только потому, что, может быть, вы в ней найдете какие-то интересные идеи для своего проекта. Например, одна из идей — в том, что HTML-разметка формы сама по себе содержит уже много информации для валидаторов (например, что из селектбокса не может быть выбран несуществующий элемент, или что в hidden-поле или в атрибуте action формы значение не может меняться волшебным образом). В HTML есть полный список полей формы, MetaForm позволяет навешивать на них мета-атрибуты, одни из них — meta:validator — очень похож по смыслу на валидаторы, описаные в статье.


    1. koorchik Автор
      15.06.2015 07:25

      Спасибо. В целом концепция такая, что есть LIVR-валидатор, как ядро, и его можно использовать в более высокоуровневых библиотеках. Например, можно написать LIVR-Forms, который, будет смотреть на поля с атрибутом data-rules и использовать LIVR для их валидации. Или же тянуть метаинформацию с самой HTML разметки (селекты и тд)


  1. antirek
    15.06.2015 07:12

    А как вы храните все ваши схемы для валидации? В каждом проекте лежит свой набор схем или как-то централизованно?


    1. koorchik Автор
      15.06.2015 07:39
      +1

      В каждом проекте свой набор схем, централизированного хранилища нет. Кроме того каждый проект имеет набор каких-то специфических дополнительных правил, например, «company_id», или «query».

      Относительно хранения схем в пределах проекта. Иногда мы описываем схемы в отдельных файлах. У нас есть инструмент для тестирования всяких REST API и там мы всегда описываем схему ожидаемого ответа в отдельных JSON файлах. И есть разные RESTful сервисы, где чаще просто непосредственно в коде и пишем правила. Если возникает необходимость переиспользовать правила, тогда можно выносить в отдельные файлы.


  1. mapron
    15.06.2015 08:32

    Объясните, пожалуйста, как вы отличаете такие два примера:
    rules:

    {
    name: 'required',
    }

    data:
    {
    name: 'REQUIRED',
    }

    result:
    {
    name: 'REQUIRED',
    }



    и
    rules:
    {
    name: 'required',
    }

    data:
    {
    name: '',
    }

    result:
    {
    name: 'REQUIRED',
    }

    ?


    1. koorchik Автор
      15.06.2015 08:39

      В livr playground по цвету (зеленый — успешная валидация, красный — ошибка) — Пример 1 и Пример 2 :)


    1. InSource
      15.06.2015 15:55

      Какую это играет роль, если, согласно статье, валидатор вернет только невалидные поля, т.е. result из второго примера? (в песочнице на сайте — на то она и песочница — будут отображены оба, различие заключается в подсветке)


  1. gonzazoid
    15.06.2015 11:56
    -1

    написал в свое время аналогичный велосипед (для ноды), добавлю пару идей — не увидел у Вас в описании рекурсивности, то есть если роверяемый параметр — объект, со своей структурой, то как его проверять? Вроде вот этого(извините что без тегов):
    var rules = {
    «contracts»: {
    «flags»: «required»,
    «type»: «object»,
    «rules»: {
    //вложенные правила
    }
    },
    «lots»: {
    «flags»: «required»,
    «type»: «object»,
    «rules»: {
    «id»: {
    «flags»: «required»,
    «type»: «number»
    },
    «contract_id»: {
    «flags»: «required»,
    «type»: «number»
    },
    «filter_id»: {
    «flags»: «required»,
    «type»: «number»
    }

    А, нет, вижу. nested_object — это оно, да?
    И второй момент, вы почему то продолжаете традиции предыдущих валидаторов, создавая сто пятсот правил вместо простого match:
    var rules = {
    «mandate»: {
    «flags»: «required»,
    «type»: «string»,
    «match»: "^[0-9a-f]{16}$",
    }
    };
    Согласитесь, добрая часть правил спокойно реализуется через регулярки, зачем для этого лепить дестки/сотни сущностей?
    И еще, по опыту. Для валидатора неплохо иметь возможность (по крайней мере мне это удобно в работе) задать политику — strict — первое же несовпадение с правилом — всех в лес, soft — все что не проходит по правилам — помечать либо возвращать в отдельном объекте.
    Дело в том, что валидатор может использоваться немного шире чем простая проверка параметров GET запроса. Это и проверка параметров функции и соответствие структуры объекта шаблону и полученные с базы данные.
    А так — целиком поддерживаю идею декларативной валидации, функциональщина в этом деле, на мой взгляд — зло.


    1. koorchik Автор
      15.06.2015 12:11

      По порядку:
      1) Все верно, вложенные объекты описываются через «nested_object». В посте раздел «Метаправила» посвящен таким правилам. Кроме того, можно добавить свои дополнительные (как в этом примере github.com/koorchik/LIVR/issues/9)
      2) Относительно регулярок. Это поддерживается, только называется «like». Но регулярки не всегда удобно читать, например легче зарегистрировать алиас «ip_address» для проверки айпишки и использовать его везде ( и возвращать какой-то понятный код ошибки ), чем везде вставлять регулярку.
      3) strict режим идея неплохая. Например, у нас один из заказчиков хотел, что если придет любой лишний параметр в REST API, например, человек опечатался, то выдавать ему сразу ошибку про неподдерживаемый параметр. Подумает над этим.


      1. gonzazoid
        15.06.2015 12:24

        >strict режим идея неплохая. Подумает над этим.
        так, на всякий случай. У себя сделал это через исключения, то есть в стрикт режиме вообще ничего не возвращается, бросает ексепшн. Удобно, не надо дополнительно проверять результаты проверки (не навязываю, просто делюсь опытом)


        1. koorchik Автор
          15.06.2015 12:38

          Исключения добавляются достаточно просто, нужно просто обернуть все правила (есть в SYNOPSYS разделе для Perl реализации — metacpan.org/pod/distribution/Validator-LIVR/README.pod ). В Perl реализации выглядит так:

          my $default_rules = Validator::LIVR->ger_default_rules();
           
          while ( my ($rule_name, $rule_builder) = each %$default_rules ) {
              Validator::LIVR->register_default_rules($rule_name => sub {
                  my $rule_validator = $rule_builder->(@_);
           
                  return sub {
                      my $error = $rule_validator->(@_);
                      die $error if $error;
                      return;
                  }
              });
          }
          


          для других реализаций должно работать аналогично


    1. VolCh
      16.06.2015 09:11
      +1

      целиком поддерживаю идею декларативной валидации, функциональщина в этом деле, на мой взгляд — зло.

      [zanuda_mode]
      Функциональное программирование — подмножество декларативного программирования.
      [/zanuda_mode]


  1. naum
    15.06.2015 17:01

    Возможно дико туплю, но где регэкспы и как их использовать?


    1. SDSWanderer
      15.06.2015 17:02

      См. правило «like»


  1. powerman
    15.06.2015 19:05

    Из спеки:

    like

    Be aware that regular expressions can be language dependent. Try to use most common syntax.
    Вам не кажется, что в таком виде это просто не имеет смысла? Декларируется независимость от языка, но регулярки везде разные, никакого «common syntax» по сути нет, даже в самых тривиальных вещах где-то пишут «a+» а где-то «a\+». Не лучше ли в спеке однозначно объявить что синтаксис регулярок должен быть, например, PCRE?

    Ещё вопрос по playground — я не разбирался с чем это связано, но в Firefox/Linux при попытке изменять значения полей в «Data for validation» после нажатия каждой кнопки заметно шуршит винт и firefox грузит CPU. Например, если в поле zip: непрерывно набирать и удалять цифры то винт трещит непрерывно и firefox ест 100% одного ядра CPU. Вроде бы проверка структуры на 5 полей, пусть даже выполняемая после ввода каждого символа, не должна так сильно грузить машину (причём не офисную, а разогнанный на 4.5GHz i7-2600K). Что и зачем при этом пишется на винт вообще не понятно — Вы что, каждое изменение по keyUp сохраняете куда-нить в localstorage?


    1. koorchik Автор
      15.06.2015 22:40

      Ответил ниже


  1. koorchik Автор
    15.06.2015 22:40

    Относительно регулярок. JS регулярки могут быть несовместимы с PCRE. Кроме того сами регулярки никак спецификацией не регламентированы. То есть, используется движок регулярных выражений, встроенный в язык. Единственное, что можно сделать в спеке — это рекомендовать не использовать очень хитроумные регулярки при описании правил, если хотите сохранить переносимость. Если переносимость не важна, тогда это не проблема.

    Относительно подтормаживаний, livr-playground в localstorage ничего не пишет. Можно поиграться с исходниками, доступны на github -https://github.com/WebbyLab/livr-playground.


    1. powerman
      16.06.2015 00:13

      Ну, Вы-ж хотите чтобы один и тот же набор правил идентично выполнялся разными языками. Если за идентичность реализации кастомных проверок отвечают их разработчики, то вот за идентичность регулярок в данный момент отвечает спека. И по текущей спеке получится так, что из-за мелкого отличия в синтаксисе регулярки возникнет необходимость держать две копии правил — для фронта и для сервера… или для RPC сервиса написанного на одном языке и клиента к нему написанном на другом.


      1. koorchik Автор
        16.06.2015 00:30

        В теории, если стремиться к полной идентичности, тогда спецификация должна описывать полный синтаксис регулярок (и предоставлять набор тестов для движков регулярных выражений), а каждая реализация должна реализовать свой движок по спецификации. На практике же, использования движка регулярных выражений, встроенного в язык, не создает проблем. Но, на всякий случай, в спеку добавили ремрку «Try to use most common syntax.». Если следовать этому совету, то не нужно будет держать две копии правил.

        Если же, на практике есть необходимость в полной идентичности, то всегда можно переопределить правило like, и разрешить в нем только определенный набор символов, например.


    1. VolCh
      16.06.2015 09:15

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


      1. koorchik Автор
        16.06.2015 10:55

        Проблема высосана из пальца:
        1) Регулярки в PHP, JS, Python и других языках очень близки. Очень редко нужны фичи регулярок, которые доступны только на одной платформе. Ок, в JS регулярках не поддерживается lookbehind, но много ли у вас таких регулярок? За такой совместимостью следить не большая проблема.
        2) Просмотрел несколько наших проектов — «like» практически нигде не нужен. Намного удобней добавить свое правило, чтобы не копипастить регулярку по 10 модулям. Например, для проверки суммы, мы не используем «like», а создаем свое правило «positive_amount». Это как вынести повторяющийся код в функцию и дать ей нормальное название. Например, у нас есть такие правила «valid_phone», «valid_url», «alphanumeric», «company_id», «uppercase_latin_symbols». Вместо всех них можно было везде писать было бы «like», но это значительно хуже в поддержке.
        3) Определение своих правил позволяет выдавать более информативные ошибки. Например, «positive_amount» у нас возвращает WRONG_AMOUNT_FORMAT, AMOUNT_TOO_SMALL, AMOUNT_TOO_LARGE. Эти ошибки затем локализируются и показываются в виде сообщений пользователю. Это намного информативней, чем просто WRONG_FORMAT, который возвращает «like».
        4) Можно написать пару тестов для кастомных правил (да и для бизнес-логики тоже), что будет полезно в любом случае.
        5) Можно переопределить «like» (или добавить какое-то свое правило, типа «match») и разрешить принимать регулярки (или просто wildcards) только в определенном формате.
        6) Указывать версии для разных платформ не даст особого выигрыша, поскольку очень низка вероятность, что в каждой версии вы решите использовать фичи доступные только на конкретной платформе. Одна из версий у вас будет работать на всех платформах, ее и оставляйте.
        7) Мир не идеален, абстракции текут, но здравый смысл всех спасет :). То есть, если не вдаваться в крайности и решать практические задачи, то все работает.
        8) Спецификация не является чем-то закрытым — она развивается, принимаются пожелания и замечания. Если при решении практических задач вы упретесь в ограничения LIVR, это будет отличным поводом для развития спецификации.
        9) У нас в компании LIVR круто себя показал, даже в тех проектах, где он используется только для бекенда. По принципу «Learn once, use everywhere».
        10) Инструмент выбирается из задач и я верю, что есть ситуации, когда LIVR может и не подойти )


  1. brdsoft
    16.06.2015 03:24

    А это нормально, что до и после email можно вводить любые символы?
    пример


    1. koorchik Автор
      16.06.2015 07:51

      В целом, да. В email разрешен юникод.


      1. brdsoft
        16.06.2015 16:45

        1. koorchik Автор
          16.06.2015 17:50

          В JS реализации не идеальная проверка. Хотя Perl-реализации полностью по RFC. Спасибо, добавим пару доп. тестов в тестовый комплект спецификации и попросим авторов реализаций обновить тесты.  


          1. koorchik Автор
            30.06.2015 10:17

            Обновили тесты, исправили, проявлялось только в JS реализации.


  1. brdsoft
    16.06.2015 03:44

    Числа между нулём и единицей почему-то не положительные


    1. koorchik Автор
      16.06.2015 07:57

      Вы правы, в случае с десятичными положительные числа начинаются не с единицы :). Спасибо — исправим.


      1. koorchik Автор
        30.06.2015 10:18

        Исправили, проявлялось только в JS реализации. Расширили тестовый комлпект для спеки.


  1. Beowulfenator
    16.06.2015 11:01

    Иногда встречается ситуация, когда правило валидации нельзя однозначно отнести к конкретному элементу формы. Например, в форме есть email и номер телефона, а для продолжения нужно заполнить хотя бы одно из этих двух полей. Позволяет ли LIVR каким-то образом описать такое правило, и если да, то к какому из полей оно относится?


    1. koorchik Автор
      16.06.2015 11:24
      +1

      Да, это возможно и делается достаточно просто:

      var LIVR = require('livr');
      
      var requiredIfFieldEmpty = function(field) {
          return function(value, fields) {
              var isDependentFieldEmpty = fields[field] === null || fields[field] === undefined || fields[field] === '';
              var isTargetFieldEmpty = value === null || value === undefined || value === '';
      
              if (isDependentFieldEmpty && isTargetFieldEmpty) {
                  return 'REQUIRED'
              }
          }
      };
      
      LIVR.Validator.registerDefaultRules({ required_if_field_empty: requiredIfFieldEmpty});
      
      var validator = new LIVR.Validator({
           phone: {required_if_field_empty: 'email'},
           email: {required_if_field_empty: 'phone'},
      });
      


      1. Beowulfenator
        16.06.2015 11:42
        +1

        Спасибо, очень интересно.


  1. koorchik Автор
    30.06.2015 10:32

    Сейчас согласовываем апдейт к LIVR v0.5 — github.com/koorchik/LIVR/labels/proposal. Оставляйте свои комментарии/замечания.

    Из нового, что уже решили добавлять в спеку — правило «any» — github.com/koorchik/LIVR/compare/v0.5.