Как стать автором
Обновить

Комментарии 6

НЛО прилетело и опубликовало эту надпись здесь
Сама задача не тривиальна — простых решений тут немного.

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

Честно говоря мне кажется что задача тривиальна.


Возможно, вы и правы


Для функций замечательно подходит функциональная композиция


Абсолютно согласен


не понятно зачем было притягивать объектную.


Это синтаксический сахар. То что можем записать так:


const objectValidator = Trava.Keys({
  a: Trava.Check(a => a >= 0),
  b: Trava.Keys({
    innerA: Trava.Check(a => a >= 0),
    innerB: Trava.Required(Trava.Check(b => b.startsWith('nice'))),
  })
});

Можем записать и так:


const objectValidator = v({
  a: ['undefined', 'non-negative'],
  b: {
    innerA: ['undefined', 'non-negative'],
    innerB: v.and('string', b => b.startsWith('nice')),
  }
});

Тогда не пришлось бы делать странные вещи вроде v.and.


Вы имеете в виду странность нейминга? Просто, вроде как, это та же функция, что и travajs/Compose.


v.explanation получается хранит глобальное состояние, это всегда боль.


Цель этой вещи собирать объяснения. Если нам понадобится их хранить — то они хранятся, если же нет — мы перед очередной проверки — очищаем этот список, с помощью v.resetExplanation() или короче v().


Обработав ошибки валидации — объяснения врядли нам нужны — поэтому то, что они будут стёрты перед следующей валидацией — не составляет проблемы.


Сейчас работает так, что не только композитор, но и сам валидатор — сам хранит объяснение, если таковое есть.


  const isValidNumber = v('number', v => `'${v}' is not a number`)
  const isValidString = v('string', v => `'${v}' is not a string`)

  isValidNumber('not valid')
  isValidString(123)

  console.log(v.explanation) // => [`'not valid' is not a number`]
  console.log(isValidNumber.explanation) // => [`'not valid' is not a number`]
  console.log(isValidString.explanation) // => [`'123' is not a string`]

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


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


Чаще всего так и будет и это приветствуется, дополнительные параметры — лишь добавляет некоторую гибкость:


  // данную взаимосвязь можно провалидировать по разному
  const row = { x: 3, squareX: 9 }

  // сверху-вниз
 const isRowValid = v(
   {
    x: 'number',
    squareX: v.and('non-negative', 'number')
   }
   ({ x, squareX }) => x ** 2 === squareX
 )

// снизу-вверх
 const isRowValid = v(
   {
    x: 'number',
    squareX: v.and(
      'non-negative', 'number',
      (squareX, { parent }) => parent.x ** 2 === squareX // или v.parent(({ x, squareX }) => x ** 2 === squareX)
   }
 )
НЛО прилетело и опубликовало эту надпись здесь

На счет состояния не могу согласиться


Я тоже не люблю состояние и предпочитаю чистые функции без побочных эффектов.


Но был выбор между двумя вариантами:


  • Валидатор возвращает объект с полным описанием проблемы(на подобие ValidationError), или null.
  • Валидатор возвращает true или false.

У первого решение — есть большие преимущества в виде фактической чистоты функций, отсутствия состояние и прочее и прочее.


У второго же варианта — есть минусы, такие как состояние, которое нужно где-то хранить, чтобы были возможны сообщения об ошибках и исправления.


Но мне валидация не выдаётся чем-то-то разтянутым по времени и месту. А значит что и хранимое состояние, и глобальные объекты — редко когда будут переживать по времени жизни — одну валидацию.


Приведу пару аргументов, почему я выбрал второй вариант:
1) Валидаторы можно использовать как предикаты без декораторов. Представим, что нам нужно свалидировать свойство во Vue:


export default {
  props: {
    messages: {
      type: Array,
      validator: v.arrayOf({ // Очень даже читаемо
        author: v.and('not-empty', 'string'),
        date: date => date instanceof Date,
        text: v.and('not-empty', 'string')
      })
    }
  }
// ...
}

Или использование в качестве аргумента в методы массива:


const arr = [1,2,3,'4','5','6']
const numbers = arr.filter(v('number'))

2) Происходит некоторое разделение обязанностей: валидатор — валидирует, объяснения объясняют, исправления исправляют


const instanceOf = constr => value => value instanceof constr
const isValidMessage = v({
  author: v(
    v.default(v.and('not-empty', 'string'), 'unknown'),
    'author name is not valid'
  ),
  date: v(
    v.default(instanceOf(Date), new Date()),
    'date is not a date'
  ),
  message: v(
    v.default('string', ''),
    'message is not a string'
  )
})
try {
  let message = await apiRequest('someApiUrl')
  v() // resetExplanation
  if (!isValidMessage(message)) { // true or false
    this.logError(new Error(v.explanation.join(', '))) // log error explanation
    message = v.fix(message) // invalidObj => validObj
  }
  return message
}

C таким разделением функционала на части — код наглядно демонстрирует последовательность:


  • валидация
  • если не валидно — лог ошибки, и исправление
  • возвращение валидного результата

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


А зачем нужна такая гибкость? Я просто убежденный сторонник подхода «если не надо — выпиливаем» и «есть только один правильный способ сделать правильно», который меня уже много лет выручает.


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


Дополнительный взгляд вверх по иерархии проверяемого объекта — вдохновлён отчасти методами массивов — которые позволяют при фильтрации — смотреть на значение массива. Например можно сделать валидатор, который по разному валидирует разные элементы массива: например элементы массива с индексами, которые являются простыми числами — должны быть простыми и тд.


Но такие случаи редки, и может даже не имеет смысла уделять им столько внимания. Чаще это всё же используется для организации разного рода дополнительного поведения: например, это используется методами исправляющими ошибки. В момент валидации валидаторы декорируемые методами-исправлениями (v.default, v.filterи v.addFix) строят дерево исправлений — там-то и используется путь от проверяемого объекта до исправляемого свойства.


Это дерево позже используется при вызове v.fix.


Но в общем случае, конечно, вы правы — решения давать больше возможностей могут приводить к возможностям стрелять себе в ногу. Подумаю как это можно улучшить.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории