Если вспомнить все ТЗ с описаниями валидации полей — они всегда выглядили примерно так:


  • не должно быть короче 6 символов
  • не должно превышать 12 символов
  • должно включать только латинские символы, цифры и знак подчёркивания

Требования часто приходят набором простых однозначных фраз. А мы, программисты, переводим эти требования в код.


Можно превращать их в одно ультимативное регулярное выражение, вроде


const validateLogin = login => /^[a-zA-z_\d]{6,12}$/.test(login);

Но лучше писать более простые функций которые легче читать и связывать с непосредственным ТЗ:


const charMatch = new RegExp('^[a-zA-Z_0-9]*$');
const validateLogin = login => {
    if (login.length < 6) return false;
    if (login.length > 12) return false;
    if (!charMatch.test(login)) return false;
    return true;
};

А что если ещё сильнее упростить этот код до чего-то вроде:


const validateLogin = login => 
  validate(login)
    .notLessThan(6)
    .notLongerThan(12)
    .hasOnly(['a-z','A-Z','0-9','_']);



Основная идея заключается в том, что требования к валидации зачастую можно разложить на список независимых типичных утверждений. А эти утвержджени (asserts) можно собрать в коллекцию и переиспользовать.


Именно этим и занимается библиотека validate.it.js. Ядро которой не превышает и ста строк и делает не так уж много:


  • позволяет вызывать assert'ы цепочно
  • собирает и обрабатывает результаты

Вышеописанное значит, что выполнив код вроде этого:


validate('Pa$$w0rd')
  .hasLettersLatin()
  .hasNumbers()
  .has("!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+");

Вы получите вот такой результат.


{
  ok: true,
  base: 'Pa$$w0rd',
  asserts: ['hasLettersLatin', 'hasNumbers', 'has'],
  errors: []
}

Думаю это и так очевидно, но немного распишу


  • ok bool — статус валидации
    • true — валидация прошла успешно
    • false — валидация провалена
  • base string — валидируемая строка
  • asserts array — список имён вызванных assert'ов в порядке очерёдности их вызова
  • errors array — массив отчётов в формате Validation Report всех провалившихся assert'ов

Вот пример провалившейся валидации


validate('bob')
  .hasLettersLatin()
  .hasNumbers();
// -->
{
  ok: false,
  base: 'bob',
  asserts: ['hasLettersLatin', 'hasNumbers'],
  errors: [
    {
      path: [],
      rule: 'hasNumbers',
      details: {
        string: 'bob',
        subStrings: ["1","2","3","4","5","6","7","8","9","0"],
        found: false,
        message: '"bob" has no numbers'
      }
    }
  ]
}

Эта простая идея позволяет писать код валидации максимально приближённым к требованиям. При этом сами assert'ы получаются очень простыми и являются чистыми функциями. Их легко поддерживать, тестировать, и добавлять новые.


Кстати да, если вам не хватает какого-то assert'а — вы легко можете на-лету добавить его через @static .extend или использовать assert .eval.


Но не жадничайте своих assert'ов сообществу. Станьте контрибьютером!


Контрибьютерам


А теперь сюрприз. Самих assert'ов в библиотеке ещё практически и нет. Только парочка базовых вроде .has, .match, .eval и ещё немного для примеров. Нету даже тех, которые я использовал в листингах этого поста. Всё дело в том, что мне хотелось привлечь внимание к идее, а не к реализации.


Более того, я считаю, что моё видение необходимых assert'ов может сильно отличаться от видения сообщества. И, делая этот инструмент "под себя" я могу сделать его неудобным для других JS разработчиков. И у меня возникла идея — привлечь к созданию этого инструмента JS сообщество. Что бы самому ничего не делать он покрывал потребности сообщества, а не мои собственные.


I need you


Я приглашаю JS разработчиков стать контрибьютерами validate.it.js.
Всех кто хочет контрибьютить в опенсорс, но не знает с какого проекта начать.
Всех кто хочет сделать для себя и для всего сообщества удобный инструмент валидации.


Вместе мы сможем наполнить коллекцию реально необходимыми всем нам assert'ами.


При этом контрибьютером может быть разработчик любого уровня. Ведь кто-то захочет заняться валидацией по длине строки, а кому-то, например, интереснее реализовывать проверку на соответствие даты всем вариантам стандарта ISO 8601.


К pull-request'ам предъявляются весьма мягкие требования:


  • наличие тестов
  • наличие описания — что бы вставить его в раздел с перечнем assert'ов.

Validation Report


Кстати о вышеупомянутых VR которыми наполняется массив .errors в случае провалившихся assert'ов. Я искал какой-то стандарт для представления ошибок валидации. И по своему опыту знал, что банального true/false и даже null/'error message' недостаточно. И судя по всему, такого стандарта на сегодняшний день нету.


Зато есть замечательная идея хабраюзера rumkin


"Универсальный интерфейс отчётов валидации" — @rumkin / Validation Report. git

Это простой DTO, который позволяет передать максимально подробную информацию о причинах провала валидации. Состоит из:


  • path array — местоположение объекта проверки. В контексте валидации строк — это всегда пустой массив [].
  • rule string — имя assert'а или правило по которому проводилась валидация.
  • details object — детали или описание в "машино-понятном виде" причины провала валидации. У этого параметра нету чётко-стандартизированный структуры, кроме одного поля:
    • details.message string — описание в "человеко-понятном виде". Это не стандартное для VR поле, но в рамках данного проекта оно обязательно.

Пример:


{
  path: [],
  rule: 'notLongerThen',
  details: {
    string: 'markopolo',
    length: 9,
    max: 6,
    message: '"markopolo" longer then 6 characters'
  }
}

rumkin, кстати, написал небольшой тул для генерации Validation Report'ов. но ради 0Dependecy я не использую конкретно эту реализацию. Благо генерация VR достаточно простая задача.


Будет здорово, если VR однажды станут стандартом, хотя бы локальным.


P.S.


Не стесняйтесь задавать вопросы — буду только рад расширить contributer's guide или ридми проекта.

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


  1. mmMike
    15.03.2018 13:20

    По моему, единственная причина использование "min|max число, максимальная длинна… и пр. — это случай, когда эти условия задаются не в коде, а в настройках. Да еще настройка рассчитана на людей, который понятия не имеют о regexp.
    В коде, лично мне, проще регулярные выражения писать, чем такую лапшу из проверок.


    1. Oborotenby
      15.03.2018 14:47
      +1

      Регэкспы могут быть абсолютно нечитабельны.


      1. mmMike
        16.03.2018 05:37

        Могут быть… На первый взгляд.


        Но когда с ними часто работаешь, то наступает просветление.
        Очень формальный и простой язык описания. Нужно только научится мыслить как парсер и просветление наступит.
        Но для тех кто код будет смотреть ВСЕГДА оставляю комментарий что данный regexp делает.


    1. BerkutEagle
      15.03.2018 17:04

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


  1. FilimoniC
    15.03.2018 19:09

    Проверка "3 условия из 4х" реализована как-нибудь?


    1. titulusdesiderio Автор
      15.03.2018 20:35

      нет. неожиданный кейс… но впринципе это легко реализуемо уже на клиенте проверкой кол-ва ошибок

      validate(string)
         .rule1()
         .rule2()
         .rule3()
         .rule4()
         .errors.length < 2;
      


  1. TheShock
    16.03.2018 03:53

    Если честно, API мне кажется не очень удачным. Сложно реюзать. Я б его инвертировал:

    passwordRules = new Validator()
      .hasLettersLatin()
      .hasNumbers();
    
    passwordRules.validate(password1);
    passwordRules.validate(password2);


    1. RubaXa
      16.03.2018 08:08

      import {composeRules, minLength, maxLength, has} from '...';
      
      const passwordRule = composeRules(
         minLength(6),
         maxLength(32),
         has(/[0-9]/, 'numbers'),
         has(/^[a-z]+$/i, 'only-latin'),
      );
      
      const result = passwordRule(inputPass.value);


    1. titulusdesiderio Автор
      16.03.2018 09:54

      а чем плох:

      const validatePassword = pass => validate(pass)
        .hasLettersLatin()
        .hasNumbers();
      
      validatePassword(password1);
      validatePassword(password2);
      


      1. RubaXa
        16.03.2018 11:46

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


        1. titulusdesiderio Автор
          16.03.2018 12:26

          можно как-то более развёрнуто?


          1. RubaXa
            16.03.2018 14:56

            Плохо тем, что на каждый чих вместо уже подготовленной (возможно оптимальной под конкретный паттерн) функции (которую барузер успешно заJITит), будет создаваться новый объект, методы которого вдобавок завраплены, т.е. как не крути, это всё оверхед.


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


            Кроме этого, если оформить в функциональном стиле, мы получим достаточно гибкую штуку и комбинации правил, типа


            import {validate, compose, minLength, regExp} import '...';
            
            const emailRule = compose('email', [
              minLength(6),
              regExp(/^.+@.+\.[a-z]+$/),
            ]);
            const onlyGmail = compose('only-gmail', [
              emailRule(),
              regExp(/@gmail.com$/),
            ]);
            
            validate(onlyGmail, input.value);


            1. titulusdesiderio Автор
              16.03.2018 15:48

              Думаю этот спор всё-равно ни к чему не приведёт, но я не вижу тут оверхеда...


              В чём конкретно проблема того что ассерты завраплены? С клиентской стороны этого не видно, а для разработки это проще. Не приходится тащить кусок ядра в каждый ассерт в отличае от вашей идеи.


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


              Да, функциональщина это круто. Но тут получается функциональщина ради функциональщины.


              Кроме этого, если оформить в функциональном стиле, мы получим ...

              … усложнение кода библиотеки, повышение порога вход, усложнение кода клиента и стоимости его поддержки, без выйгрыша в чём либо.
              Теоретически compose вместе с хитрым createComplexRule могут дать какой-то прирост в производительности по сравнению с runAssertAsMethod. Но это уж точно выглядит оверхедом и преждевременной оптимизацией


          1. RubaXa
            16.03.2018 15:08

            Хотя думаю погорячился, так что всё это ИМХО, просто у себя я именно так и сделал, вот:


            Правила
            export function minLength(min: number): ValidateRule {
                return ({value}) => value.length >= min ? null : {
                    id: 'minLength',
                    detail: {
                        min,
                    },
                };
            }
            
            export function required(): ValidateRule {
                return createComplexRule('required', {}, [minLength(1)]);
            }
            
            export function email(): ValidateRule {
                return createComplexRule('email', {}, [regexp(/^.+@.+\..+$/)]);
            }
            
            export function password(additionalRules: ValidateRule[] = []): ValidateRule {
                return createComplexRule('password', {}, [
                    required(),
                    minLength(6),
                ].concat(additionalRules));
            }