jnv
v0.8.0
Published
JSON Validator
Downloads
175
Readme
🎲 jnv | JSON Validator
Быстрый и расширяемый JSON-Валидатор.
- Простая декларативная схема.
- Смешанная декларация моделей
JsonLike + *Model<JsonLike>. - Регистрация ошибок.
- Композиция валидаторов через
.pipe(). - Расширяемый API.
- Поддержка
TypeScript. - Без зависимостей(кроме
js-base-error).
Установка
npm i jnvjnv использует "peerDependencies": { "js-base-error": "0.7.0" }.
Использование peerDependency гарантирует, что все части вашего приложения (включая jnv и другие библиотеки) будут использовать один и тот же экземпляр базового класса ошибок BaseError.
Если ваш проект не использует js-base-error, библиотека установится автоматически, иначе проверьте совместимость версии [email protected] в вашем package.json.
🔥 Пример использования
import { Factory } from 'jnv'
const v = new Factory()
const userModel = v.obj({
id: v.positive(), // эквивалентно int().min(1)
name: v.str().min(3),
email: /^[0-9a-z]+@[0-9a-z]+\.[a-z]+$/i, // эквивалентно v.re(...)
gender: v.enum('male', 'female').optional(),
// Оборачиваем объект в тип, для возможности применения .stopError() - игнорировать
// ошибку и установить значение по умолчанию или `null`(если его нет).
address: v.obj({
city: v.str().nonempty(), // эквивалентно .str().min(1)
street: 'any string', // эквивалентно .str()
zipCode: ''
}).stopError()
}).rename('User') // полезно для регистрации ошибки
const sampleUser = {
id: 1,
name: 'John',
email: '[email protected]',
// gender: 'male' as ('male' | 'female'),
address: {
city: 'Springfield',
street: '742 Evergreen Terrace',
zipCode: '90210'
}
}
expect(userModel.validate(sampleUser)).toStrictEqual({ ok: true, value: sampleUser })API(Кратко)
Базовые типы - методы Factory
- of() - Автоматический парсер.
- raw() - Необработанный
JsonLike, объект не проверяется и возвращается как есть. - bool() -
boolean. - num()/int()/nonnegative()/positive()/range() -
number. - str()/nonempty()/re() -
string - literal()/enum() - Литералы(примитивы
null|bollean|number|string). - union() - Один из вариантов
JsonLike. - obj() -
PlainObject(не массив). - arr() - Массивы
JsonLike[]. - tuple() - Эмуляция
Tuple. - custom() - Пользовательская функция.
- pipe() - Цепочка
Model<JsonLike>.
Модификаторы типа значения - методы Model
- min()/max()/range() - Для чисел, строк и массивов.
- nonempty() - Непустая строка или массив.
Опции - методы Model
- def() - Установить значение по умолчанию.
- optional() - Аналог необязательного свойства TS
{ prop?: type }. - stopError() - Подавить ошибку и заменить на значение по умолчанию для недопустимого типа.
- removeFaulty() - Удалить недопустимый элемент массива.
- rename() - Установить имя модели.
- freeze() - Запрещает дальнейшие расширения типа.
Результат - методы Model
validate() - Валидация с гарантированным подавлением ошибки и результатом {ok,value,error,warning}
validateOrThrow() - Вернуть тип или поднять ошибку.
{
ok: boolean,
value?: T, // только если ok: true
error?: JnvError, // только если ok: false
warning?: JnvError // только если нет error и ok:true
}Копирование или перезапись выходного объекта зависит от параметра конфигурации
{createMode:'all'|'obj'|'arr'|'none'}.
Больше примеров
Необязательно определять каждому вложенному типу собственный класс модели, если он не использует модификаторы. Любой Json-тип будет выведен из его JS-типа:
// Это ...
v.arr([v.obj({...})])
// ... эквивалентно
v.arr([{...}])Регулярное выражение
RegExpприводится к типуstringс проверкой по регулярному выражению:{foo: /re/} => {foo: v.re(/re/, ...)}. Любая функция приводится кCustomModel.
Единственные типы которые невозможно вывести, это литералы. Литералы допускают только Json-примитивы:
const abbr = v.literal('ABBR')
const enabled = v.enum(false, true, 0, 1, 'off', 'on')Как заменить недопустимый тип массива:
// Параметры конфигурации доступны через JSDoc.
const v = new Factory({
createMode: 'obj' // создадим только объекты, массивы перезапишем
})
const arrModel = v.arr([
v.obj({ enabled: v.enum('on', 'off') })
.def({ AhHaHa: '😋' }) // значение по умолчанию
.stopError() // подавить ошибку
]).freeze()
expect(arrModel.validate([
{ enabled: 'on' },
{ enabled: 'oh no' },
{ enabled: 'off' }
]).value).toStrictEqual([
{ enabled: 'on' },
{ AhHaHa: '😋' },
{ enabled: 'off' }
])Как пропустить невалидный элемент:
const arrCleared = v.arr([
v.obj({ enabled: v.enum('on', 'off') })
]).removeFaulty() // удалить невалидное значение
expect(arrCleared.validate([
{ enabled: 'on' },
{ enabled: 'oh no' },
{ enabled: 'off' }
]).value).toStrictEqual([
{ enabled: 'on' },
{ enabled: 'off' }
])Расширение классов
Более подробное описание расширения пользовательских типов можно увидеть на этой странице dev.md
Класс валидатора обязан реализовать единственный абстрактный метод _validate(ctx, value)
import {
type Context,
type Re,
type TRes,
StrMetadata,
Model,
Factory,
isString
} from 'jnv'
class PhoneNumberModel extends Model<TJsonLike, any> {
// Подскажем TS более конкретный тип Metadata
declare protected readonly _meta: StrMetadata & { re: readonly Re[] }
protected _validate (ctx: Context, value: any): TRes<string> {
if (!isString(value)) {
return ctx.faultyValueMessage(this.kind, 'Expected a string.', value)
}
// Получим ссылку на Metadata и проверим варианты RegExp
for (const re of this._meta.re) {
if (re.test(value)) {
return { ok: true, value }
}
}
return ctx.faultyValueMessage(this.kind, 'Invalid phone number format.', value)
}
}Расширяем фабрику валидаторов:
class MyFactory extends Factory {
// Добавим к фабрике новый тип
phoneNumber (): PhoneNumberModel {
// Используем кеш regExp
const re = this._regExpCache.getOf(/^\d{3}-\d{3}-\d{4}$/)
const meta = new StrMetadata(null, null, false, [re])
// Последний параметр false - это ключ isFrozen и здесь он не нужен.
// Model будет автоматически привязана к свойству объекта.
return new PhoneNumberModel(this._config, this._defaultOptions, meta, false)
}
}Используем наш валидатор:
const v = new MyFactory()
const phoneModel = v.phoneNumber()
expect(phoneModel.validate('123-456-7890'))
.toStrictEqual({ ok: true, value: '123-456-7890' })
expect(phoneModel.validate('123-456-789').error!.message)
.toContain('Invalid phone number format.')