# Обучение

# Простой пример

v9s создает цепочку правил. Вы можете интегрировать собственные правила в цепочку при помощи метода use или внедрить расширения при помощи метода inject. Для валидации значение вызовите метод check.

Простой пример:

Как видите, если ошибок нет, то будет возвращено undefined.

# Сообщения об ошибках

Когда вы импортируете библиотеку - вы импортируете функцию def, которая устанавливает тип для сообщений об ошибках и опциональное значение ошибки по умолчанию. По умолчанию метод check возвращает T | undefined, где T - тип значения сообщения об ошибке. Для того, чтобы использовать строковые сообщения об ошибках, просто установите типом сообщения (T) string.

Пример:

import { v9s } from 'v9s';

// создаем экземпляр валидатора с правилами и сообщениями об ошибках.
const validator = v9s<string>('invalid value').lte(100).gte(10);

const small = validator.check(1); // проверяем маленькое значение

console.log(small); // 'invalid value'

const big = validator.check(110); // проверяем большое значение

console.log(big); // 'invalid value'

const normal = validator.check(50); // проверяем нормальное значение

console.log(normal); // undefined

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

import { v9s } from 'v9s';

// создаем экземпляр валидатора с правилами и сообщениями об ошибках.
const validator = v9s<string>().lte(100, 'too big').gte(10, 'too small');

const small = validator.check(1); // проверяем маленькое значение

console.log(small); // 'too small'

const big = validator.check(110); // проверяем большое значение

console.log(big); // 'too big'

const normal = validator.check(50); // проверяем нормальное значение

console.log(normal); // undefined

ПРЕДУПРЕЖДЕНИЕ

Если значение сообщения об ошибке по умолчанию не задано и не задано сообщение об ошибке для какого либо правила в цепочке - будет выброшено исключение.

import { v9s } from 'v9s';

// create a validator instance with rules.
const validator = v9s<string>().lte(100).gte(10);

const normal = validator.check(50); // проверяем нормальное значение

console.log(normal); // undefined

const small = validator.check(1); // Уппс! Error('Undefined default negative value')

Если вам нужно использовать другой формат сообщения - задайте его тип:

import { v9s } from 'v9s';

enum ValidationError {
  tooSmall,
  tooBig
}

const validator = v9s.lte<ValidationError>(100, ValidationError.tooBig).gte(10, ValidationError.tooSmall);

const small = validator.check(1); // проверяем маленькое значение

console.log(small); // 0

const big = validator.check(110); // проверяем большое значение

console.log(big); // 1

const normal = validator.check(50); // проверяем нормальное значение

console.log(normal); // undefined

# Упрощение результата

Порой достаточно получить булево значение без других специальных типов или undefined в качестве результата. Для таких целей библиотека предоставляет специальную обертку экземпляра валидатора. Функция simplify возвращает функцию с сигнатурой, соответствующей сигнатуре метода check:

type CheckFunc<T> = (value: any, context: any) => T | undefined;

Пример:

import { v9s, simplify } from 'v9s';

const check = simplify(v9s(false).lte(100).gte(10));

const small = check(1); // проверяем маленькое значение

console.log(small); // false

const big = check(110); // проверяем большое значение

console.log(big); // false

const normal = check(50); // проверяем нормальное значение

console.log(normal); // true

# Последовательности

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

import { v9s } from 'v9s';

// создаем экземпляр валидатора с сортированной цепочкой правил и сообщениями об ошибках.
const validator = v9s<string>().gte(10, 'very small').gte(100, 'small');

const verySmall = validator.check(9); // проверяем очень маленькое значение

console.log(verySmall); // 'very small'

const small = validator.check(50); // проверяем маленькое значение

console.log(small); // 'small'

const normal = validator.check(110); // проверяем нормальное значение

console.log(normal); // undefined

# Инверсия

Иногда нужно инвертировать результат работы правила. Это легко делается при помощи метода not:

import { v9s } from 'v9s';

// создадим экземпляр валидатора с инвертированным правилом
const validator = v9s(false).not().string();

const isNumber = validator.check(42); // проверяем число

console.log(isNumber); // undefined

const isString = validator.check('42'); // проверяем строку

console..log(isString); // false (не строка)

# Модификатор опциональности

Также можно разрешить undefined значения:

import { v9s } from 'v9s';

const validator = v9s(false).string().optional();

const isNumber = validator.check(42); // проверяем число

console.log(isNumber); // false

const isString = validator.check('42'); // проверяем строку

console.log(isString); // undefined

const isNotDefined = validator.check(undefined); // проверяем undefined

console.log(isNotDefined); // undefined

ПРЕДУПРЕЖДЕНИЕ

Модификатор optional применяется только к конкретному правилу, следующее правило его игнорирует.

# Композиция

Когда необходимо добавить альтернативное условие - самое время использовать метод or:

import { v9s, simplify } from 'v9s';

const check = simplify(v9s(false).string().optional().or(v9s(false).number()));

const isString = validator.check('42');

console.log(isString); // true

const isNotDefined = validator.check(undefined);

console.log(isNotDefined); // true

const isNumber = validator.check(42);

console.log(isNumber); // true

const isBoolean = validator.check(true);

console.log(isBoolean); // false

const isNull = validator.check(null);

console.log(isNull); // false, потому что null !== undefined и может быть корректным значением

Обычно не требуется сохранять экземпляр валидатора, только функцию проверки:

import { v9s } from 'v9s';

const check = v9s(false).string().optional().or(v9s(false).number()).check;

console.log(check('42')); // undefined

# Внешние правила

Конечно, библиотека содержит минимальное количество правил внутри, но можно использовать внешние правила. Внешнее правило должно быть совместимо с этой сигнатурой:

type Rule = (value: any, context: any) => boolean;

Давайте создадим собственное правило, которое проверяет, является ли строковое значение целым числом.

import { v9s, simplify } from 'v9s';

const integer = (value: string) => /^[0-9]+$/.test(value); // проверяем целочисленную строку
const check = simplify(v9s(false).use(integer));

console.log(check('42')); // true
console.log(check('42a')); // false

# Модификаторы

Хорошо, мы уверены, что наше значение является целочисленной строкой. Теперь добавим диапазон допустимых значений и преобразуем значение в тип number при помощи Modifier:

import { v9s, simplify } from 'v9s';

const integer = (value: string) => /^[0-9]+$/.test(value);
const modify = (value: string) => Number(value); // пробразование сроки в число
const check = simplify(v9s(false).use(integer, undefined, modify).between(10, 100));

console.log(check('42')); // true
console.log(check('9')); // false
console.log(check('110')); // false

Сигнатура модификатора:

type Modifier = (value: any, context: any) => any;

# Инъекции

В отличие от внешних правил, инъекции позволяют указать другую цепочку через экземпляр валидатора или функцию с сигнатурой, аналогичной сигнатуре метода check. Эта цепочка будет проверяться перед основной. Например, напишем примитивную инъекцию each:

import { v9s, CheckFunc, Message, MessageFactory, Validator } from 'v9s';

function each<T>(chain: CheckFunc<T> | Validator<T>, message: Message<T>): CheckFunc<T> {
  return (value: any, context: any = {}) => {
    const getMessage = () => (typeof message === 'function' ? (message as MessageFactory<T>)() : message);
    const check = typeof chain === 'function' ? chain : chain.check;

    if (!Array.isArray(value)) return getMessage();
    else
      return value.reduce<T | undefined>((prev, current) => (prev === undefined ? check(current, context) : prev), undefined);
  };
}

const check = v9s<string>().inject(
  each(v9s<string>().number('not a number').gte(2, 'too small').lte(10, 'too big'), 'not array')
).check;

console.log(check('[1, 2, 3]')); // 'not array'
console.log(check(['1', '2', '3'])); // 'not a number'
console.log(check([1, 2, 3, 11])); // 'too small'
console.log(check([2, 3, 11])); // 'too big'
console.log(check([2, 3])); // undefined
}

ПРИМЕЧАНИЕ

Проверка полных схем не является целью v9s, но, как видите, это возможно.

# Интернационализация

Вместо строковых сообщений можно использовать фабрики сообщений. Эта функция может быть полезна для интернационализированных приложений.

import { v9s } from 'v9s';

enum Lang {
  de,
  en,
  ru
}

let lang: Lang = Lang.en;

const errorMessageFactory = () => {
  switch (lang) {
    case Lang.de:
      return 'Ungültiger Wert';
    case Lang.ru:
      return 'Неверное значение';
    default:
      return 'Invalid value';
  }
};

const check = v9s<string>().between(10, 100, errorMessageFactory).check;

console.log(check(50)); // undefined
console.log(check(110)); // 'Invalid value'

lang = Lang.de;

console.log(check(110)); // 'Ungültiger Wert'

lang = Lang.ru;

console.log(check(110)); // 'Неверное значение'

ПРИМЕЧАНИЕ

Если вам нужно получать функции как сообщения об ошибках, задайте сообщения через фабрики: () => errorMessageFunction.

# Возврат объекта

В некоторых случаях вы можете захотеть получить объект с полем, отражающим состояние, вместо чистого результата или undefined. Обертка objectify заставляет цепочку возвращать экземпляр следующего класса:

/**
 * Успешный или неудачный результат валидации.
 */
export class ValidationResult<T> {
  /**
   * Сообщение об ошибке.
   */
  public readonly error?: T;

  /**
   * Состояние результата валидации.
   */
  public readonly success: boolean;

  constructor(error?: T) {
    this.error = error;
    this.success = error === undefined;
  }
}

Пример:

import { v9s, objectify } from 'v9s';

const check = objectify(v9s('invalid').number('not a number').gte(10).lte(100));

const isString = check('42');

console.log(isString.success); // false
console.log(isString.error); // 'not a number'

const tooSmall = check(5);

console.log(tooSmall.success); // false
console.log(tooSmall.error); // 'invalid'

const tooBig = check(110);

console.log(tooBig.success); // false
console.log(tooBig.error); // 'invalid'

const normal = check(50);

console.log(normal.success); // true
console.log(normal.error); // undefined

# Контекст

Вы видели параметр context в предыдущих примерах. Это объект (по умолчанию: {}), который перемещается между правилами в цепочке и позволяет обмениваться данными между ними. Контекст может содержать промежуточные вычисления, другие поля субъекта и так далее. В следующем примере промежуточные вычисления перемещаются между правилами:

import { v9s, simplify } from 'v9s';

const checkForDuplicates = function (value: number[], context: { sorted?: number[] }) {
  const sorted = value.slice().sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));

  const noDuplicates = sorted.every((v, i) => !i || sorted[i - 1] !== v);

  if (noDuplicates) context.sorted = sorted;

  return noDuplicates;
};

const checkMinimum = function (minimum: number, value: number[], context: { sorted: number[] }) {
  const { sorted } = context;

  return sorted.length > 0 && minimum <= sorted[0];
};

const checkMaximum = function (maximum: number, value: number[], context: { sorted: number[] }) {
  const { sorted } = context;

  return sorted.length > 0 && maximum >= sorted[sorted.length - 1];
};

const check = simplify(
  v9s(false).use(checkForDuplicates).use(checkMinimum.bind(undefined, 10)).use(checkMaximum.bind(undefined, 100))
);

console.log(check([])); // false - пустой
console.log(check([1, 6, 4, 2, 1])); // false - дубликаты `1`
console.log(check([1, 6, 4, 2])); // false - 1 < 10
console.log(check([10, 60, 105, 40, 20])); // false - 105 > 100
console.log(check([10, 60, 40, 20])); // true

Еще один вариант применения контекста - условная проверка с привязкой к другим полям объекта. Поля value и name интерфейса имеют значение только тогда, когда оба не пусты. В следующем примере аргумент контекста вручную передается в функцию check.

import { v9s, simplify } from 'v9s';

interface Data {
  name: string;
  value: string;
}

const checkNameRule = function (value: string, context: Data) {
  return (!value && !context.value) || value.length > 0;
};

const checkValueRule = function (value: string, context: Data) {
  return (!value && !context.name) || /^[0-9]+$/.test(value);
};

const checkName = simplify(v9s(false).use(checkNameRule));
const checkValue = simplify(v9s(false).use(checkValueRule));

const empty = { name: '', value: '' };

console.log(checkName(empty.name, empty), checkValue(empty.value, empty)); // true, true

const emptyName = { name: '', value: '42' };

console.log(checkName(emptyName.name, emptyName), checkValue(emptyName.value, emptyName)); // false, true

const emptyValue = { name: 'the answer', value: '' };

console.log(checkName(emptyValue.name, emptyValue), checkValue(emptyValue.value, emptyValue)); // true, false

const filled = { name: 'the answer', value: '42' };

console.log(checkName(filled.name, filled), checkValue(filled.value, filled)); // true, true