schemastery
Advanced tools
Comparing version 3.14.7 to 3.15.0
@@ -1,2 +0,2 @@ | ||
import { Dict } from 'cosmokit'; | ||
import { Binary, Dict } from 'cosmokit'; | ||
declare const kSchema: unique symbol; | ||
@@ -10,3 +10,3 @@ declare global { | ||
type TypeT<X> = ReturnType<From<X>>; | ||
type Resolve = (data: any, schema: Schema, options?: Options, strict?: boolean) => [any, any?]; | ||
type Resolve = (data: any, schema: Schema, options: Options, strict?: boolean) => [any, any?]; | ||
type IntersectS<X> = From<X> extends Schema<infer S, unknown> ? S : never; | ||
@@ -30,3 +30,3 @@ type IntersectT<X> = Inverse<From<X>> extends ((arg: infer T) => void) ? T : never; | ||
extend(type: string, resolve: Resolve): void; | ||
any(): Schema<any>; | ||
any<T = any>(): Schema<T>; | ||
never(): Schema<never>; | ||
@@ -40,2 +40,5 @@ const<const T>(value: T): Schema<T>; | ||
date(): Schema<string | Date, Date>; | ||
regExp(flag?: string): Schema<string | RegExp, RegExp>; | ||
arrayBuffer(): Schema<Binary.Source, ArrayBufferLike>; | ||
arrayBuffer(encoding: 'hex' | 'base64'): Schema<Binary.Source | string, ArrayBufferLike>; | ||
bitset<K extends string>(bits: Partial<Record<K, number>>): Schema<number | readonly K[], number>; | ||
@@ -50,3 +53,4 @@ function(): Schema<Function, (...args: any[]) => any>; | ||
intersect<const X>(list: readonly X[]): Schema<IntersectS<X>, IntersectT<X>>; | ||
transform<X, T>(inner: X, callback: (value: TypeS<X>) => T, preserve?: boolean): Schema<TypeS<X>, T>; | ||
transform<X, T>(inner: X, callback: (value: TypeS<X>, options: Schemastery.Options) => T, preserve?: boolean): Schema<TypeS<X>, T>; | ||
ValidationError: typeof ValidationError; | ||
} | ||
@@ -56,2 +60,3 @@ interface Options { | ||
ignore?(data: any, schema: Schema): boolean; | ||
path?: (keyof any)[]; | ||
} | ||
@@ -124,4 +129,10 @@ interface Meta<T = any> { | ||
} | ||
declare class ValidationError extends TypeError { | ||
options: Schemastery.Options; | ||
name: string; | ||
constructor(message: string, options: Schemastery.Options); | ||
static is(error: any): error is ValidationError; | ||
} | ||
type Schema<S = any, T = S> = Schemastery<S, T>; | ||
declare const Schema: Schemastery.Static; | ||
export = Schema; |
{ | ||
"name": "schemastery", | ||
"description": "Type driven schema validator", | ||
"version": "3.14.7", | ||
"version": "3.15.0", | ||
"main": "lib/index.cjs", | ||
@@ -6,0 +6,0 @@ "module": "lib/index.mjs", |
163
src/index.ts
@@ -1,4 +0,5 @@ | ||
import { clone, deepEqual, Dict, filterKeys, isNullable, isPlainObject, pick, valueMap } from 'cosmokit' | ||
import { Binary, clone, deepEqual, Dict, filterKeys, isNullable, isPlainObject, pick, valueMap } from 'cosmokit' | ||
const kSchema = Symbol.for('schemastery') | ||
const kValidationError = Symbol.for('ValidationError') | ||
@@ -22,3 +23,3 @@ declare global { | ||
export type TypeT<X> = ReturnType<From<X>> | ||
export type Resolve = (data: any, schema: Schema, options?: Options, strict?: boolean) => [any, any?] | ||
export type Resolve = (data: any, schema: Schema, options: Options, strict?: boolean) => [any, any?] | ||
@@ -41,3 +42,3 @@ export type IntersectS<X> = From<X> extends Schema<infer S, unknown> ? S : never | ||
extend(type: string, resolve: Resolve): void | ||
any(): Schema<any> | ||
any<T = any>(): Schema<T> | ||
never(): Schema<never> | ||
@@ -51,2 +52,5 @@ const<const T>(value: T): Schema<T> | ||
date(): Schema<string | Date, Date> | ||
regExp(flag?: string): Schema<string | RegExp, RegExp> | ||
arrayBuffer(): Schema<Binary.Source, ArrayBufferLike> | ||
arrayBuffer(encoding: 'hex' | 'base64'): Schema<Binary.Source | string, ArrayBufferLike> | ||
bitset<K extends string>(bits: Partial<Record<K, number>>): Schema<number | readonly K[], number> | ||
@@ -61,3 +65,4 @@ function(): Schema<Function, (...args: any[]) => any> | ||
intersect<const X>(list: readonly X[]): Schema<IntersectS<X>, IntersectT<X>> | ||
transform<X, T>(inner: X, callback: (value: TypeS<X>) => T, preserve?: boolean): Schema<TypeS<X>, T> | ||
transform<X, T>(inner: X, callback: (value: TypeS<X>, options: Schemastery.Options) => T, preserve?: boolean): Schema<TypeS<X>, T> | ||
ValidationError: typeof ValidationError | ||
} | ||
@@ -68,2 +73,3 @@ | ||
ignore?(data: any, schema: Schema): boolean | ||
path?: (keyof any)[] | ||
} | ||
@@ -140,6 +146,33 @@ | ||
class ValidationError extends TypeError { | ||
name = 'ValidationError' | ||
constructor(message: string, public options: Schemastery.Options) { | ||
let prefix = '$' | ||
for (const segment of options.path || []) { | ||
if (typeof segment === 'string') { | ||
prefix += '.' + segment | ||
} else if (typeof segment === 'number') { | ||
prefix += '[' + segment + ']' | ||
} else if (typeof segment === 'symbol') { | ||
prefix += `[Symbol(${segment.toString()})]` | ||
} | ||
} | ||
if (prefix.startsWith('.')) prefix = prefix.slice(1) | ||
super((prefix === '$' ? '' : `${prefix} `) + message) | ||
} | ||
static is(error: any): error is ValidationError { | ||
return !!error?.[kValidationError] | ||
} | ||
} | ||
Object.defineProperty(ValidationError.prototype, kValidationError, { | ||
value: true, | ||
}) | ||
type Schema<S = any, T = S> = Schemastery<S, T> | ||
const Schema = function (options: Schema) { | ||
const schema = function (data: any, options?: Schemastery.Options) { | ||
const schema = function (data: any, options: Schemastery.Options = {}) { | ||
return Schema.resolve(data, schema, options)[0] | ||
@@ -179,2 +212,4 @@ } as Schema | ||
Schema.ValidationError = ValidationError | ||
let refs: Record<number, Schema> | undefined | ||
@@ -321,3 +356,3 @@ | ||
try { | ||
Schema.resolve(value, schema) | ||
Schema.resolve(value, schema, {}) | ||
return schema.simplify(value) | ||
@@ -361,3 +396,3 @@ } catch {} | ||
if (isNullable(data)) { | ||
if (schema.meta.required) throw new TypeError(`missing required value`) | ||
if (schema.meta.required) throw new ValidationError(`missing required value`, options) | ||
let current = schema | ||
@@ -374,3 +409,3 @@ let fallback = schema.meta.default | ||
const callback = resolvers[schema.type] | ||
if (!callback) throw new TypeError(`unsupported type "${schema.type}"`) | ||
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options) | ||
@@ -416,5 +451,5 @@ try { | ||
Schema.is(Date), | ||
Schema.transform(Schema.string().role('datetime'), (value) => { | ||
Schema.transform(Schema.string().role('datetime'), (value, options) => { | ||
const date = new Date(value) | ||
if (isNaN(+date)) throw new TypeError(`invalid date "${value}"`) | ||
if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options) | ||
return date | ||
@@ -425,2 +460,35 @@ }, true), | ||
Schema.regExp = function regExp(flag = '') { | ||
return Schema.union([ | ||
Schema.is(RegExp), | ||
Schema.transform(Schema.string().role('regexp', { flag }), (value, options) => { | ||
try { | ||
return new RegExp(value, flag) | ||
} catch (e: any) { | ||
throw new ValidationError(e.message, options) | ||
} | ||
}, true), | ||
]) | ||
} | ||
Schema.arrayBuffer = function arrayBuffer(encoding?: 'hex' | 'base64') { | ||
return Schema.union([ | ||
Schema.is(ArrayBuffer), | ||
Schema.is(SharedArrayBuffer), | ||
Schema.transform(Schema.any<ArrayBufferView>(), (value, options) => { | ||
if (Binary.isSource(value)) return Binary.fromSource(value) | ||
throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options) | ||
}, true), | ||
...encoding ? [Schema.transform(Schema.string(), (value, options) => { | ||
try { | ||
return encoding === 'base64' | ||
? Binary.fromBase64(value) | ||
: Binary.fromHex(value) | ||
} catch (e: any) { | ||
throw new ValidationError(e.message, options) | ||
} | ||
}, true)] as const : [], | ||
]) | ||
} | ||
Schema.extend('any', (data) => { | ||
@@ -430,24 +498,24 @@ return [data] | ||
Schema.extend('never', (data) => { | ||
throw new TypeError(`expected nullable but got ${data}`) | ||
Schema.extend('never', (data, _, options) => { | ||
throw new ValidationError(`expected nullable but got ${data}`, options) | ||
}) | ||
Schema.extend('const', (data, { value }) => { | ||
if (data === value) return [value] | ||
throw new TypeError(`expected ${value} but got ${data}`) | ||
Schema.extend('const', (data, { value }, options) => { | ||
if (deepEqual(data, value)) return [value] | ||
throw new ValidationError(`expected ${value} but got ${data}`, options) | ||
}) | ||
function checkWithinRange(data: number, meta: Schemastery.Meta<any>, description: string, skipMin = false) { | ||
function checkWithinRange(data: number, meta: Schemastery.Meta<any>, description: string, options: Schemastery.Options, skipMin = false) { | ||
const { max = Infinity, min = -Infinity } = meta | ||
if (data > max) throw new TypeError(`expected ${description} <= ${max} but got ${data}`) | ||
if (data < min && !skipMin) throw new TypeError(`expected ${description} >= ${min} but got ${data}`) | ||
if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options) | ||
if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options) | ||
} | ||
Schema.extend('string', (data, { meta }) => { | ||
if (typeof data !== 'string') throw new TypeError(`expected string but got ${data}`) | ||
Schema.extend('string', (data, { meta }, options) => { | ||
if (typeof data !== 'string') throw new ValidationError(`expected string but got ${data}`, options) | ||
if (meta.pattern) { | ||
const regexp = new RegExp(meta.pattern.source, meta.pattern.flags) | ||
if (!regexp.test(data)) throw new TypeError(`expect string to match regexp ${regexp}`) | ||
if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options) | ||
} | ||
checkWithinRange(data.length, meta, 'string length') | ||
checkWithinRange(data.length, meta, 'string length', options) | ||
return [data] | ||
@@ -477,8 +545,8 @@ }) | ||
Schema.extend('number', (data, { meta }) => { | ||
if (typeof data !== 'number') throw new TypeError(`expected number but got ${data}`) | ||
checkWithinRange(data, meta, 'number') | ||
Schema.extend('number', (data, { meta }, options) => { | ||
if (typeof data !== 'number') throw new ValidationError(`expected number but got ${data}`, options) | ||
checkWithinRange(data, meta, 'number', options) | ||
const { step } = meta | ||
if (step && !isMultipleOf(data, meta.min ?? 0, step)) { | ||
throw new TypeError(`expected number multiple of ${step} but got ${data}`) | ||
throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options) | ||
} | ||
@@ -488,8 +556,8 @@ return [data] | ||
Schema.extend('boolean', (data) => { | ||
Schema.extend('boolean', (data, _, options) => { | ||
if (typeof data === 'boolean') return [data] | ||
throw new TypeError(`expected boolean but got ${data}`) | ||
throw new ValidationError(`expected boolean but got ${data}`, options) | ||
}) | ||
Schema.extend('bitset', (data, { bits, meta }) => { | ||
Schema.extend('bitset', (data, { bits, meta }, options) => { | ||
let value = 0, keys: string[] = [] | ||
@@ -506,7 +574,7 @@ if (typeof data === 'number') { | ||
for (const key of keys) { | ||
if (typeof key !== 'string') throw new TypeError(`expected string but got ${key}`) | ||
if (typeof key !== 'string') throw new ValidationError(`expected string but got ${key}`, options) | ||
if (key in bits!) value |= bits![key]! | ||
} | ||
} else { | ||
throw new TypeError(`expected number or array but got ${data}`) | ||
throw new ValidationError(`expected number or array but got ${data}`, options) | ||
} | ||
@@ -517,15 +585,18 @@ if (value === meta.default) return [value] | ||
Schema.extend('function', (data) => { | ||
Schema.extend('function', (data, _, options) => { | ||
if (typeof data === 'function') return [data] | ||
throw new TypeError(`expected function but got ${data}`) | ||
throw new ValidationError(`expected function but got ${data}`, options) | ||
}) | ||
Schema.extend('is', (data, { callback }) => { | ||
Schema.extend('is', (data, { callback }, options) => { | ||
if (data instanceof callback!) return [data] | ||
throw new TypeError(`expected ${callback!.name} but got ${data}`) | ||
throw new ValidationError(`expected ${callback!.name} but got ${data}`, options) | ||
}) | ||
function property(data: any, key: keyof any, schema: Schema, options?: Schemastery.Options) { | ||
function property(data: any, key: keyof any, schema: Schema, options: Schemastery.Options) { | ||
try { | ||
const [value, adapted] = Schema.resolve(data[key], schema, options) | ||
const [value, adapted] = Schema.resolve(data[key], schema, { | ||
...options, | ||
path: [...options.path || [], key], | ||
}) | ||
if (adapted !== undefined) data[key] = adapted | ||
@@ -541,4 +612,4 @@ return value | ||
Schema.extend('array', (data, { inner, meta }, options) => { | ||
if (!Array.isArray(data)) throw new TypeError(`expected array but got ${data}`) | ||
checkWithinRange(data.length, meta, 'array length', !isNullable(inner!.meta.default)) | ||
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options) | ||
checkWithinRange(data.length, meta, 'array length', options, !isNullable(inner!.meta.default)) | ||
return [data.map((_, index) => property(data, index, inner!, options))] | ||
@@ -548,3 +619,3 @@ }) | ||
Schema.extend('dict', (data, { inner, sKey }, options, strict) => { | ||
if (!isPlainObject(data)) throw new TypeError(`expected object but got ${data}`) | ||
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options) | ||
const result: any = {} | ||
@@ -554,3 +625,3 @@ for (const key in data) { | ||
try { | ||
rKey = Schema.resolve(key, sKey!)[0] | ||
rKey = Schema.resolve(key, sKey!, options)[0] | ||
} catch (error) { | ||
@@ -568,3 +639,3 @@ if (strict) continue | ||
Schema.extend('tuple', (data, { list }, options, strict) => { | ||
if (!Array.isArray(data)) throw new TypeError(`expected array but got ${data}`) | ||
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options) | ||
const result = list!.map((inner, index) => property(data, index, inner, options)) | ||
@@ -584,3 +655,3 @@ if (strict) return [result] | ||
Schema.extend('object', (data, { dict }, options, strict) => { | ||
if (!isPlainObject(data)) throw new TypeError(`expected object but got ${data}`) | ||
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options) | ||
const result: any = {} | ||
@@ -606,3 +677,3 @@ for (const key in dict) { | ||
} | ||
throw new TypeError(`expected ${toString()} but got ${JSON.stringify(data)}`) | ||
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options) | ||
}) | ||
@@ -618,7 +689,7 @@ | ||
} else if (typeof result !== typeof value) { | ||
throw new TypeError(`expected ${toString()} but got ${JSON.stringify(data)}`) | ||
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options) | ||
} else if (typeof value === 'object') { | ||
merge(result ??= {}, value) | ||
} else if (result !== value) { | ||
throw new TypeError(`expected ${toString()} but got ${JSON.stringify(data)}`) | ||
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options) | ||
} | ||
@@ -625,0 +696,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
122397
1993