Комментарии 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
.
Но в общем случае, конечно, вы правы — решения давать больше возможностей могут приводить к возможностям стрелять себе в ногу. Подумаю как это можно улучшить.
Как строить и построить