schemastery
Advanced tools
Comparing version 2.0.0 to 2.1.0
@@ -1,3 +0,3 @@ | ||
declare type Dict<T = any> = { | ||
[key: string]: T; | ||
declare type Dict<T = any, K extends string = string> = { | ||
[key in K]?: T; | ||
}; | ||
@@ -10,2 +10,3 @@ declare type Intersect<U> = (U extends any ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I : never; | ||
new (data?: null): T; | ||
[kSchema]: true; | ||
toJSON(): Schema.Base<T>; | ||
@@ -20,7 +21,11 @@ required(): Schema<S, T>; | ||
export declare namespace Schema { | ||
export type TypeS<X> = X extends Schema<infer S, unknown> ? S : never; | ||
export type TypeT<X> = ReturnType<Extract<X, Schema>>; | ||
export type From<T> = T extends string | number | boolean ? Schema<T> : T extends Schema ? T : T extends typeof String ? Schema<string> : T extends typeof Number ? Schema<number> : T extends typeof Boolean ? Schema<boolean> : T extends Constructor<infer S> ? Schema<S> : never; | ||
type _TypeS<X> = X extends Schema<infer S, unknown> ? S : never; | ||
type _TypeT<X> = ReturnType<Extract<X, Schema>>; | ||
export type TypeS<X> = _TypeS<From<X>>; | ||
export type TypeT<X> = _TypeT<From<X>>; | ||
export type Resolve = (data: any, schema?: Schema, strict?: boolean) => [any, any?]; | ||
export interface Base<T = any> { | ||
type: string; | ||
sKey?: Schema; | ||
inner?: Schema; | ||
@@ -41,11 +46,19 @@ list?: Schema[]; | ||
} | ||
type TupleS<X extends readonly unknown[]> = X extends readonly [infer L, ...infer R] ? [TypeS<L>?, ...TupleS<R>] : any[]; | ||
type TupleT<X extends readonly unknown[]> = X extends readonly [infer L, ...infer R] ? [TypeT<L>?, ...TupleT<R>] : any[]; | ||
type ObjectS<X extends Dict<Schema>> = { | ||
type TupleS<X extends readonly any[]> = X extends readonly [infer L, ...infer R] ? [TypeS<L>?, ...TupleS<R>] : any[]; | ||
type TupleT<X extends readonly any[]> = X extends readonly [infer L, ...infer R] ? [TypeT<L>?, ...TupleT<R>] : any[]; | ||
type ObjectS<X extends Dict> = { | ||
[K in keyof X]?: TypeS<X[K]>; | ||
} & Dict; | ||
type ObjectT<X extends Dict<Schema>> = { | ||
type ObjectT<X extends Dict> = { | ||
[K in keyof X]?: TypeT<X[K]>; | ||
} & Dict; | ||
export interface Types { | ||
type Constructor<T = any> = new (...args: any[]) => T; | ||
export interface Static { | ||
<T = any>(options: Base<T>): Schema<T>; | ||
new <T = any>(options: Base<T>): Schema<T>; | ||
prototype: Schema; | ||
resolve: Resolve; | ||
from<T>(source: T): Schema<From<T>>; | ||
property(data: any, key: keyof any, schema?: Schema): any; | ||
extend(type: string, resolve: Resolve): void; | ||
any(): Schema<any>; | ||
@@ -57,22 +70,15 @@ never(): Schema<never>; | ||
boolean(): Schema<boolean>; | ||
array<S, T>(inner: Schema<S, T>): Schema<S[], T[]>; | ||
dict<S, T>(inner: Schema<S, T>): Schema<Dict<S>, Dict<T>>; | ||
tuple<X extends readonly Schema[]>(list: X): Schema<TupleS<X>, TupleT<X>>; | ||
object<X extends Dict<Schema>>(dict: X): Schema<ObjectS<X>, ObjectT<X>>; | ||
union<X extends Schema>(list: readonly X[]): Schema<TypeS<X>, TypeT<X>>; | ||
intersect<X extends Schema>(list: readonly X[]): Schema<Intersect<TypeS<X>>, Intersect<TypeT<X>>>; | ||
transform<S, T>(inner: Schema<S>, callback: (value: S) => T): Schema<S, T>; | ||
is<T>(constructor: Constructor<T>): Schema<T>; | ||
array<X>(inner: X): Schema<TypeS<X>[], TypeT<X>[]>; | ||
dict<X, Y extends string | Schema<any, string>>(inner: X, sKey?: Y): Schema<Dict<TypeS<X>, TypeS<Y>>, Dict<TypeT<X>, TypeT<Y>>>; | ||
tuple<X extends readonly any[]>(list: X): Schema<TupleS<X>, TupleT<X>>; | ||
object<X extends Dict>(dict: X): Schema<ObjectS<X>, ObjectT<X>>; | ||
union<X>(list: readonly X[]): Schema<TypeS<X>, TypeT<X>>; | ||
intersect<X>(list: readonly X[]): Schema<Intersect<TypeS<X>>, Intersect<TypeT<X>>>; | ||
transform<X, T>(inner: X, callback: (value: TypeS<X>) => T): Schema<TypeS<X>, T>; | ||
} | ||
export interface Static extends Types { | ||
prototype: Schema; | ||
resolve: Resolve; | ||
property(data: any, key: keyof any, schema?: Schema): any; | ||
extend<K extends keyof Types>(type: K, resolve: Resolve, keys?: (keyof Base)[], meta?: Meta): void; | ||
select<T extends string>(values: T[] | Record<T, string>): Schema<T>; | ||
<T = any>(options: Base<T>): Schema<T>; | ||
new <T = any>(options: Base<T>): Schema<T>; | ||
} | ||
export {}; | ||
} | ||
declare const kSchema: unique symbol; | ||
export declare const Schema: Schema.Static; | ||
export default Schema; |
138
lib/index.js
@@ -10,5 +10,3 @@ "use strict"; | ||
} | ||
function valueMap(object, transform) { | ||
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)])); | ||
} | ||
const kSchema = Symbol('schemastery'); | ||
exports.Schema = function (options) { | ||
@@ -24,2 +22,3 @@ const schema = function (data) { | ||
exports.Schema.prototype = Object.create(Function.prototype); | ||
exports.Schema.prototype[kSchema] = true; | ||
exports.Schema.prototype.toJSON = function toJSON() { | ||
@@ -45,16 +44,4 @@ return { ...this }; | ||
const resolvers = {}; | ||
exports.Schema.extend = function extend(type, resolve, keys, meta) { | ||
exports.Schema.extend = function extend(type, resolve) { | ||
resolvers[type] = resolve; | ||
if (!keys) | ||
return; | ||
Object.assign(exports.Schema, { | ||
[type](...args) { | ||
const schema = new exports.Schema({ type }); | ||
keys.forEach((key, index) => { | ||
schema[key] = args[index]; | ||
}); | ||
Object.assign(schema.meta, meta); | ||
return schema; | ||
}, | ||
}); | ||
}; | ||
@@ -83,8 +70,30 @@ exports.Schema.resolve = function resolve(data, schema, hint) { | ||
}; | ||
exports.Schema.from = function from(source) { | ||
if (isNullable(source)) { | ||
return exports.Schema.any(); | ||
} | ||
else if (['string', 'number', 'boolean'].includes(typeof source)) { | ||
return exports.Schema.const(source).required(); | ||
} | ||
else if (source[kSchema]) { | ||
return source; | ||
} | ||
else if (typeof source === 'function') { | ||
switch (source) { | ||
case String: return exports.Schema.string(); | ||
case Number: return exports.Schema.number(); | ||
case Boolean: return exports.Schema.boolean(); | ||
default: return exports.Schema.is(source); | ||
} | ||
} | ||
else { | ||
throw new TypeError(`cannot infer schema from ${source}`); | ||
} | ||
}; | ||
exports.Schema.extend('any', (data) => { | ||
return [data]; | ||
}, []); | ||
}); | ||
exports.Schema.extend('never', (data) => { | ||
throw new TypeError(`expected nullable but got ${data}`); | ||
}, []); | ||
}); | ||
exports.Schema.extend('const', (data, { value }) => { | ||
@@ -94,3 +103,3 @@ if (data === value) | ||
throw new TypeError(`expected ${value} but got ${data}`); | ||
}, ['value']); | ||
}); | ||
exports.Schema.extend('string', (data) => { | ||
@@ -100,3 +109,3 @@ if (typeof data === 'string') | ||
throw new TypeError(`expected string but got ${data}`); | ||
}, []); | ||
}); | ||
exports.Schema.extend('number', (data) => { | ||
@@ -106,3 +115,3 @@ if (typeof data === 'number') | ||
throw new TypeError(`expected number but got ${data}`); | ||
}, []); | ||
}); | ||
exports.Schema.extend('boolean', (data) => { | ||
@@ -112,3 +121,8 @@ if (typeof data === 'boolean') | ||
throw new TypeError(`expected boolean but got ${data}`); | ||
}, []); | ||
}); | ||
exports.Schema.extend('is', (data, { callback }) => { | ||
if (data instanceof callback) | ||
return [data]; | ||
throw new TypeError(`expected instance of ${callback.name} but got ${data}`); | ||
}); | ||
exports.Schema.extend('array', (data, { inner }) => { | ||
@@ -118,8 +132,23 @@ if (!Array.isArray(data)) | ||
return [data.map((_, index) => exports.Schema.property(data, index, inner))]; | ||
}, ['inner']); | ||
exports.Schema.extend('dict', (data, { inner }) => { | ||
}); | ||
exports.Schema.extend('dict', (data, { inner, sKey }, strict) => { | ||
if (!isObject(data)) | ||
throw new TypeError(`expected dict but got ${data}`); | ||
return [valueMap(data, (_, key) => exports.Schema.property(data, key, inner))]; | ||
}, ['inner']); | ||
const result = {}; | ||
for (const key in data) { | ||
let rKey; | ||
try { | ||
rKey = exports.Schema.resolve(key, sKey)[0]; | ||
} | ||
catch (error) { | ||
if (strict) | ||
continue; | ||
throw error; | ||
} | ||
result[rKey] = exports.Schema.property(data, key, inner); | ||
data[rKey] = data[key]; | ||
delete data[key]; | ||
} | ||
return [result]; | ||
}); | ||
exports.Schema.extend('tuple', (data, { list }, strict) => { | ||
@@ -133,3 +162,3 @@ if (!Array.isArray(data)) | ||
return [result]; | ||
}, ['list']); | ||
}); | ||
function merge(result, data) { | ||
@@ -155,3 +184,3 @@ for (const key in data) { | ||
return [result]; | ||
}, ['dict']); | ||
}); | ||
exports.Schema.extend('union', (data, { list }) => { | ||
@@ -168,3 +197,3 @@ const messages = []; | ||
throw new TypeError(`expected union but got ${JSON.stringify(data)}`); | ||
}, ['list']); | ||
}); | ||
exports.Schema.extend('intersect', (data, { list }) => { | ||
@@ -179,3 +208,3 @@ const result = {}; | ||
return [result]; | ||
}, ['list']); | ||
}); | ||
exports.Schema.extend('transform', (data, { inner, callback }) => { | ||
@@ -197,11 +226,42 @@ const [result, adapted = data] = exports.Schema.resolve(data, inner, true); | ||
} | ||
}, ['inner', 'callback']); | ||
exports.Schema.select = function select(values) { | ||
if (Array.isArray(values)) | ||
values = Object.fromEntries(values.map((value) => [value, value])); | ||
const list = Array.isArray(values) | ||
? values.map((value) => exports.Schema.const(value).description(value)) | ||
: Object.entries(values).map(([key, value]) => exports.Schema.const(key).description(value)); | ||
return exports.Schema.union(list); | ||
}; | ||
}); | ||
function defineMethod(name, keys) { | ||
Object.assign(exports.Schema, { | ||
[name](...args) { | ||
const schema = new exports.Schema({ type: name }); | ||
keys.forEach((key, index) => { | ||
switch (key) { | ||
case 'sKey': | ||
schema.sKey = exports.Schema.from(args[index]); | ||
break; | ||
case 'inner': | ||
schema.inner = exports.Schema.from(args[index]); | ||
break; | ||
case 'list': | ||
schema.list = args[index].map(exports.Schema.from); | ||
break; | ||
case 'dict': | ||
schema.dict = Object.fromEntries(Object.entries(args[index]).map(([key, value]) => [key, exports.Schema.from(value)])); | ||
break; | ||
default: schema[key] = args[index]; | ||
} | ||
}); | ||
return schema; | ||
}, | ||
}); | ||
} | ||
defineMethod('is', ['callback']); | ||
defineMethod('any', []); | ||
defineMethod('never', []); | ||
defineMethod('const', ['value']); | ||
defineMethod('string', []); | ||
defineMethod('number', []); | ||
defineMethod('boolean', []); | ||
defineMethod('array', ['inner']); | ||
defineMethod('dict', ['inner', 'sKey']); | ||
defineMethod('tuple', ['list']); | ||
defineMethod('object', ['dict']); | ||
defineMethod('union', ['list']); | ||
defineMethod('intersect', ['list']); | ||
defineMethod('transform', ['inner', 'callback']); | ||
exports.default = exports.Schema; |
{ | ||
"name": "schemastery", | ||
"description": "Yet another schema validator", | ||
"version": "2.0.0", | ||
"version": "2.1.0", | ||
"main": "index.js", | ||
@@ -6,0 +6,0 @@ "typings": "lib/index.d.ts", |
129
README.md
# Schemastery | ||
[](https://www.npmjs.com/package/schemastery) | ||
Yet another schema validator. | ||
@@ -7,5 +9,5 @@ | ||
- **Lightweight.** No dependencies. | ||
- **Lightweight.** Zero dependencies. | ||
- **Easy to use.** You can use any schema as a function or constructor directly. | ||
- **Powerful.** Schemastery supports some advanced types such as `transform`. | ||
- **Powerful.** Schemastery supports some advanced types such as `union`, `intersect` and `transform`. | ||
- **Extensible.** You can create your own schema types via `Schema.extend()`. | ||
@@ -34,3 +36,3 @@ - **Serializable.** Schema objects can be serialized into JSON and then be hydrated in another environment. | ||
interface Config { | ||
foo?: 'red' | 'blue' | ||
foo: Record<string, string> | ||
bar: string[] | ||
@@ -40,12 +42,12 @@ } | ||
const Config = Schema.object({ | ||
foo: Schema.select(['red', 'blue']).default('red'), | ||
bar: Schema.array(Schema.string()), | ||
foo: Schema.dict(Schema.string()).default({}), | ||
bar: Schema.array(Schema.string()).default([]), | ||
}) | ||
// config is an instance of Config | ||
// in this case, that is { foo: red, bar: [] } | ||
// in this case, that is { foo: {}, bar: [] } | ||
const config = new Config() | ||
``` | ||
## Builtin Types | ||
## Basic Types | ||
@@ -76,2 +78,13 @@ ### Schema.any() | ||
### Schema.const(value) | ||
Assert that the value is equal to the given constant. | ||
```js | ||
const validate = Schema.const(10) | ||
validate(10) // 10 | ||
validate(0) // TypeError | ||
``` | ||
### Schema.number() | ||
@@ -86,3 +99,2 @@ | ||
validate(1) // 1 | ||
validate(Number()) // 0 | ||
validate('') // TypeError | ||
@@ -101,3 +113,2 @@ ``` | ||
validate('foo') // 'foo' | ||
validate(String()) // '' | ||
``` | ||
@@ -115,5 +126,16 @@ | ||
validate(true) // true | ||
validate(Boolean()) // false | ||
``` | ||
### Schema.is(constructor) | ||
Assert that the value is an instance of the given constructor. | ||
```js | ||
const validate = Schema.is(RegExp) | ||
validate() // undefined | ||
validate(/foo/) // /foo/ | ||
validate('foo') // TypeError | ||
``` | ||
### Schema.array(value) | ||
@@ -216,3 +238,3 @@ | ||
validaate() // 1 | ||
validate() // 1 | ||
validate('0') // TypeError | ||
@@ -222,6 +244,85 @@ validate(10) // 11 | ||
## Instance Methods | ||
### schema.required() | ||
Assert that the value is not nullable. | ||
### schema.default(value) | ||
Set the fallback value when nullable. | ||
### schema.description(text) | ||
Set the description of the schema. | ||
## Shorthand Syntax | ||
Some shorthand syntax is available for inner types. | ||
- `Schema.any()` -> `undefined` | ||
- `Schema.string()` -> `String` | ||
- `Schema.number()` -> `Number` | ||
- `Schema.boolean()` -> `Boolean` | ||
- `Schema.const(1)` -> `1` (only for primitive types) | ||
- `Schema.is(Date)` -> `Date` | ||
```js | ||
Schema.array(String) // Schema.array(Schema.string()) | ||
Schema.union([1, 2]) // Schema.union([Schema.const(1), Schema.const(2)]) | ||
Schema.dict(RegExp) // Schema.dict(Schema.is(RegExp)) | ||
``` | ||
## Advanced Examples | ||
Here are some examples which demonstrate how to define advanced types. | ||
### Enumeration | ||
```js | ||
const Enum = Schema.union(['red', 'blue']) | ||
Enum('red') // 'red' | ||
Enum('blue') // 'blue' | ||
Enum('green') // TypeError | ||
``` | ||
### ToString | ||
```js | ||
const ToString = Schema.transform(Schema.any(), v => String(v)) | ||
ToString('') // '' | ||
ToString(0) // '0' | ||
ToString({}) // '{}' | ||
``` | ||
### Listable | ||
```js | ||
const Listable = Schema.union([ | ||
Schema.array(Schema.number()), | ||
Schema.transform(Schema.number(), n => [n]), | ||
]).default([]) | ||
Listable() // [] | ||
Listable(0) // [0] | ||
Listable([1, 2]) // [1, 2] | ||
``` | ||
### Alias | ||
```js | ||
const Config = Schema.dict(Schema.number(), Schema.union([ | ||
'foo', | ||
Schema.transform('bar', () => 'foo'), | ||
])) | ||
Config({ foo: 1 }) // { foo: 1 } | ||
Config({ bar: 2 }) // { foo: 2 } | ||
Config({ bar: '3' }) // TypeError | ||
``` | ||
## Extensibility | ||
Use `Schema.extend()` to create a new type. | ||
## Serializability | ||
@@ -228,0 +329,0 @@ |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
42436
336
330
0