UNPKG

lib0

Version:

isomorphic utility functions

1,647 lines (1,516 loc) • 49.1 kB
/** * @experimental WIP * * Simple & efficient schemas for your data. */ import * as arr from './array.js' import * as buffer from './buffer.js' import * as error from './error.js' import * as equalityTraits from './trait/equality.js' import * as fun from './function.js' import * as string from './string.js' import * as prng from './prng.js' import * as number from './number.js' import * as map from './map.js' import { global } from './environment.js' import * as object from './object.js' /** * @typedef {string|number|bigint|boolean|null|undefined|symbol} Primitive */ /** * @typedef {{ [k:string|number|symbol]: any }} AnyObject */ /** * @template T * @typedef {T extends Schema<infer X> ? X : T} Unwrap */ /** * @template T * @typedef {T extends Schema<infer X> ? X : T} TypeOf */ /** * @template {readonly unknown[]} T * @typedef {T extends readonly [Schema<infer First>, ...infer Rest] ? [First, ...UnwrapArray<Rest>] : [] } UnwrapArray */ /** * @template T * @typedef {T extends Schema<infer S> ? Schema<S> : never} CastToSchema */ /** * @template {unknown[]} Arr * @typedef {Arr extends [...unknown[], infer L] ? L : never} TupleLast */ /** * @template {unknown[]} Arr * @typedef {Arr extends [...infer Fs, unknown] ? Fs : never} TuplePop */ /** * @template {readonly unknown[]} T * @typedef {T extends [] * ? {} * : T extends [infer First] * ? First * : T extends [infer First, ...infer Rest] * ? First & Intersect<Rest> * : never * } Intersect */ export class ValidationError { constructor () { /** * Reverse errors * @type {Array<{ path: string?, expected: string, has: string, message: string? }>} */ this._rerrs = [] } /** * @param {string?} path * @param {string} expected * @param {string} has * @param {string?} message */ extend (path, expected, has, message = null) { this._rerrs.push({ path, expected, has, message }) } toString () { const s = [] for (let i = this._rerrs.length - 1; i >= 0; i--) { const r = this._rerrs[i] /* c8 ignore next */ s.push(string.repeat(' ', (this._rerrs.length - i) * 2) + `${r.path != null ? `[${r.path}] ` : ''}${r.has} doesn't match ${r.expected}. ${r.message}`) } return s.join('\n') } } /** * @param {any} a * @param {any} b * @return {boolean} */ /* @__NO_SIDE_EFFECTS__ */ const _extendsShapeHelper = (a, b) => { if (a === b) return true if (a == null || b == null || a.constructor !== b.constructor) return false if (a[equalityTraits.EqualityTraitSymbol]) return equalityTraits.equals(a, b) // last resort: check equality (do this before array and obj check which don't implement the equality trait) if (arr.isArray(a)) { return arr.every(a, aitem => arr.some(b, bitem => _extendsShapeHelper(aitem, bitem)) ) } else if (object.isObject(a)) { return object.every(a, (aitem, akey) => _extendsShapeHelper(aitem, b[akey]) ) } /* c8 ignore next */ return false } /** * @param {Schema<any>} a * @param {Schema<any>} b */ /* @__NO_SIDE_EFFECTS__ */ export const extendsShape = (a, b) => { const aShape = /** @type {any} */ (a).shape const bShape = /** @type {any} */ (b).shape return /** @type {any} */ (a.constructor)._dilutes ? _extendsShapeHelper(bShape, aShape) : _extendsShapeHelper(aShape, bShape) } const schemaSymbol = Symbol.for('schema:Schema') /** * @template T * @implements {equalityTraits.EqualityTrait} */ export class Schema { get isSchema () { return schemaSymbol } // this.shape must not be defined on Schema. Otherwise typecheck on metatypes (e.g. $$object) won't work as expected anymore /** * If true, the more things are added to the shape the more objects this schema will accept (e.g. * union). By default, the more objects are added, the the fewer objects this schema will accept. * @protected */ static _dilutes = false /** * Overwrite this when necessary. By default, we only check the `shape` property which every shape * should have. * @param {Schema<any>} other */ equals (other) { // @ts-ignore return this.constructor === other.constructor && fun.equalityDeep(this.shape, other.shape) } /** * @param {object} other */ [equalityTraits.EqualityTraitSymbol] (other) { return this.equals(/** @type {any} */ (other)) } /** * Use `schema.validate(obj)` with a typed parameter that is already of typed to be an instance of * Schema. Validate will check the structure of the parameter and return true iff the instance * really is an instance of Schema. * * @param {T} o * @return {boolean} */ validate (o) { return this.check(o) } /* c8 ignore start */ /** * Similar to validate, but this method accepts untyped parameters. * * @param {any} _o * @param {ValidationError} [_err] * @return {_o is T} */ check (_o, _err) { error.methodUnimplemented() } /* c8 ignore stop */ /** * @type {Schema<T?>} */ get nullable () { // @ts-ignore return $union(this, $null) } /** * @type {$Optional<Schema<T>>} */ get optional () { return new $Optional(/** @type {Schema<T>} */ (this)) } /** * Cast a variable to a specific type. Returns the casted value, or throws an exception otherwise. * Use this if you know that the type is of a specific type and you just want to convince the type * system. * * @template OO * @param {OO} o * @return {Extract<OO, T> extends never ? T : (OO extends Array<never> ? T : Extract<OO,T>)} */ cast (o) { assert(o, this) return /** @type {any} */ (o) } /** * EXPECTO PATRONUM!! 🪄 * This method protects against type errors. * * "After all this time?" * "Always." - Snape, talking about type safety * * Ensures that a variable is a a specific type. Returns the value, or throws an exception if the assertion check failed. * * Can be useful when defining lambdas: `s.lambda(s.$number, s.$number).expect((n) => n + 1)` * * @param {T} o * @return {T extends T ? T : never} */ expect (o) { assert(o, this) return o } } /** * @extends {Schema<any>} */ export class $Type extends Schema { /** * @param {string} name */ constructor (name) { super() /** * @type {string} */ this.name = name this.typeSymbol = Symbol('schema:' + name) } /** * @param {any} o * @param {ValidationError} err * @return {o is any} */ check (o, err) { const c = o != null && o.$type === this /* c8 ignore next */ !c && err != null && err.extend(null, 'type check on', o?.constructor.name, 'failed') return c } } /** * A shared gobal namespace to store schemas across lib0 installations */ const schemaStore = /* @__PURE__ */(() => { const _ns = '_lib0@v1/$' return (global[_ns] = (global[_ns] || new Map())) })() /** * Define a schema for a type that can validate instances across duplicated module installations. * * You may only assign one "$type" to any class. * * The recommended way to assign a type to a class is by doing. While not strictly necessary - it ensures proper typing. * * const $a = A.prototype.$type = s.$type(':a', A) * * Use $type if you need to validate duplicated classes from other (duplicate) installations of your * js module. Don't use this only if needed and not as some "standard practice". * * It is good practice to define a $type property on the type constructor to enable efficient * typechecks `myClassInstance.$type === $myClass` * * @template {new (...args:any[])=>any} T * @param {string} uniqueName - a good name would be `{projectPrefix}:{ConstructorName}[@v{version}]` * @param {T|{ [K:string|number|symbol]:any }|null} TypeConstructor * @return {Schema<InstanceType<T>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $type = (uniqueName, TypeConstructor) => { const type = map.setIfUndefined(schemaStore, uniqueName, () => new $Type(uniqueName)) if (TypeConstructor) { /* c8 ignore next */ if (TypeConstructor.prototype && object.hasProperty(TypeConstructor.prototype, '$type')) { error.unexpectedCase() } (TypeConstructor.prototype || TypeConstructor).$type = type } return /** @type {any} */ (type) } export const $$type = $Type.prototype.$type = $type('s:$type', $Type) /** * @template {(new (...args:any[]) => any) | ((...args:any[]) => any)} Constr * @typedef {Constr extends ((...args:any[]) => infer T) ? T : (Constr extends (new (...args:any[]) => any) ? InstanceType<Constr> : never)} Instance */ /** * @template {(new (...args:any[]) => any) | ((...args:any[]) => any)} C * @extends {Schema<Instance<C>>} */ export class $ConstructedBy extends Schema { /** * @param {C} c * @param {((o:Instance<C>)=>boolean)|null} check */ constructor (c, check) { super() this.shape = c this._c = check } /** * @param {any} o * @param {ValidationError} [err] * @return {o is C extends ((...args:any[]) => infer T) ? T : (C extends (new (...args:any[]) => any) ? InstanceType<C> : never)} o */ check (o, err = undefined) { /* c8 ignore next */ const c = o?.constructor === this.shape && (this._c == null || this._c(o)) /* c8 ignore next */ !c && err?.extend(null, this.shape.name, o?.constructor.name, o?.constructor !== this.shape ? 'Constructor match failed' : 'Check failed') return c } } /** * @template {(new (...args:any[]) => any) | ((...args:any[]) => any)} C * @param {C} c * @param {((o:Instance<C>) => boolean)|null} check * @return {CastToSchema<$ConstructedBy<C>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $constructedBy = (c, check = null) => new $ConstructedBy(c, check) export const $$constructedBy = $type('s:$ConstructedBy', $ConstructedBy) /** * Check custom properties on any object. You may want to overwrite the generated Schema<any>. * * @extends {Schema<any>} */ export class $Custom extends Schema { /** * @param {(o:any,err?:ValidationError) => boolean} check */ constructor (check) { super() /** * @type {(o:any,err?:ValidationError) => boolean} */ this.shape = check } /** * @param {any} o * @param {ValidationError} [err] * @return {o is any} */ check (o, err) { const c = this.shape(o, err) /* c8 ignore next */ !c && err?.extend(null, 'custom prop', o?.constructor.name, 'failed to check custom prop') return c } } /** * @param {(o:any,err?:ValidationError) => boolean} check * @return {Schema<any>} */ /* @__NO_SIDE_EFFECTS__ */ export const $custom = (check) => new $Custom(check) export const $$custom = $type('s:$Custom', $Custom) /** * @template {Primitive} T * @extends {Schema<T>} */ export class $Literal extends Schema { /** * @param {Array<T>} literals */ constructor (literals) { super() this.shape = literals } /** * * @param {any} o * @param {ValidationError} [err] * @return {o is T} */ check (o, err) { const c = this.shape.some(a => a === o) /* c8 ignore next */ !c && err?.extend(null, this.shape.join(' | '), o + '') return c } } /** * @template {Primitive[]} T * @param {T} literals * @return {CastToSchema<$Literal<T[number]>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $literal = (...literals) => new $Literal(literals) export const $$literal = $type('s:$Literal', $Literal) /** * Meta schema that matches a `$Literal` whose shape is exactly `literals` - e.g. * `$$literalWith(null).check($null)` or `.if($$literalWith(true), ..)` in a pattern matcher. * * @template {Primitive[]} T * @param {T} literals * @return {Schema<CastToSchema<$Literal<T[number]>>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $$literalWith = (...literals) => $custom((o, err) => $$literal.check(o, err) && o.shape.length === literals.length && arr.every(o.shape, (l, i) => l === literals[i])) /** * @template {Array<string|Schema<string|number>>} Ts * @typedef {Ts extends [] ? `` : (Ts extends [infer T] ? (Unwrap<T> extends (string|number) ? Unwrap<T> : never) : (Ts extends [infer T1, ...infer Rest] ? `${Unwrap<T1> extends (string|number) ? Unwrap<T1> : never}${Rest extends Array<string|Schema<string|number>> ? CastStringTemplateArgsToTemplate<Rest> : never}` : never))} CastStringTemplateArgsToTemplate */ /** * @param {string} str * @return {string} */ /* c8 ignore start - else should be ignored */ const _regexEscape = /* @__PURE__ */(() =>/** @type {any} */ (RegExp).escape || /** @type {(str:string) => string} */ ( str => str.replace(/[().|&,$^[\]]/g, s => '\\' + s)) )() /* c8 ignore stop */ /** * @param {string|Schema<any>} s * @return {string[]} */ /* @__NO_SIDE_EFFECTS__ */ const _schemaStringTemplateToRegex = s => { if ($string.check(s)) { return [_regexEscape(s)] } if ($$literal.check(s)) { return /** @type {Array<string|number>} */ (s.shape).map(v => v + '') } if ($$number.check(s)) { return ['[+-]?\\d+.?\\d*'] } if ($$string.check(s)) { return ['.*'] } if ($$union.check(s)) { return s.shape.map(_schemaStringTemplateToRegex).flat(1) } /* c8 ignore next 3 */ // unexpected schema structure (only supports unions and string in literal types) error.unexpectedCase() } /** * @template {Array<string|Schema<string|number>>} T * @extends {Schema<CastStringTemplateArgsToTemplate<T>>} */ export class $StringTemplate extends Schema { /** * @param {T} shape */ constructor (shape) { super() this.shape = shape this._r = new RegExp('^' + shape.map(_schemaStringTemplateToRegex).map(opts => `(${opts.join('|')})`).join('') + '$') } /** * @param {any} o * @param {ValidationError} [err] * @return {o is CastStringTemplateArgsToTemplate<T>} */ check (o, err) { const c = this._r.exec(o) != null /* c8 ignore next */ !c && err?.extend(null, this._r.toString(), o + '', 'String doesn\'t match string template.') return c } } /** * @template {Array<string|Schema<string|number>>} T * @param {T} literals * @return {CastToSchema<$StringTemplate<T>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $stringTemplate = (...literals) => new $StringTemplate(literals) export const $$stringTemplate = $type('s:$StringTemplate', $StringTemplate) /** * @template {Schema<any>} S * @extends Schema<Unwrap<S>|undefined> */ export class $Optional extends Schema { /** * @param {S} shape */ constructor (shape) { super() this.shape = shape } /** * @param {any} o * @param {ValidationError} [err] * @return {o is (Unwrap<S>|undefined)} */ check (o, err) { const c = o === undefined || this.shape.check(o) /* c8 ignore next */ !c && err?.extend(null, 'undefined (optional)', '()') return c } } export const $$optional = $type('s:$Optional', $Optional) /** * @extends Schema<never> */ class $Never extends Schema { /** * @param {any} _o * @param {ValidationError} [err] * @return {_o is never} */ check (_o, err) { /* c8 ignore next */ err?.extend(null, 'never', typeof _o) return false } } /** * @type {Schema<never>} */ export const $never = /* @__PURE__ */new $Never() export const $$never = /* @__PURE__ */$type('s:$Never', $Never) /** * @template {{ [key: string|symbol|number]: Schema<any> }} S * @typedef {{ [Key in keyof S as S[Key] extends $Optional<Schema<any>> ? Key : never]?: S[Key] extends $Optional<Schema<infer Type>> ? Type : never } & { [Key in keyof S as S[Key] extends $Optional<Schema<any>> ? never : Key]: S[Key] extends Schema<infer Type> ? Type : never }} $ObjectToType */ /** * @template {{[key:string|symbol|number]: Schema<any>}} S * @extends {Schema<$ObjectToType<S>>} */ export class $Object extends Schema { /** * @param {S} shape * @param {boolean} partial */ constructor (shape, partial = false) { super() /** * @type {S} */ this.shape = shape this._isPartial = partial } static _dilutes = true /** * @type {Schema<Partial<$ObjectToType<S>>>} */ get partial () { return new $Object(this.shape, true) } /** * @param {any} o * @param {ValidationError} err * @return {o is $ObjectToType<S>} */ check (o, err) { if (o == null) { /* c8 ignore next */ err?.extend(null, 'object', 'null') return false } return object.every(this.shape, (vv, vk) => { const c = (this._isPartial && !object.hasProperty(o, vk)) || vv.check(o[vk], err) !c && err?.extend(vk.toString(), vv.toString(), typeof o[vk], 'Object property does not match') return c }) } } /** * @template S * @typedef {import('./ts.js').Prettify<{ [Key in keyof S as S[Key] extends $Optional<Schema<any>> ? Key : never]?: S[Key] extends $Optional<Schema<infer Type>> ? Type : never } & { [Key in keyof S as S[Key] extends $Optional<Schema<any>> ? never : Key]: S[Key] extends Schema<infer Type> ? Type : never }, 1>} _ObjectDefToSchema */ /** * @template {{ [key:string|symbol|number]: any }} S * @param {S} def * @return {Schema<_ObjectDefToSchema<S>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $object = def => /** @type {any} */ (new $Object(def)) export const $$object = /* @__PURE__ */$type('s:$Object', $Object) /** * @template {{ [key:string|symbol|number]: any }} S * @param {S} def * @return {Schema<Partial<_ObjectDefToSchema<S>>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $partial = def => /** @type {any} */ (new $Object(def, true)) /** * Matches plain objects only - `{}`-literals and `Object.create(null)` objects, not class * instances. The check is prototype-based, not `o.constructor === Object`, because a plain * object may declare an own property named "constructor". * * @type {Schema<{[key:string]: any}>} */ export const $objectAny = $custom(o => { if (o == null) return false const proto = Object.getPrototypeOf(o) return proto === Object.prototype || proto === null }) /** * @template {Schema<string|number|symbol>} Keys * @template {Schema<any>} Values * @extends {Schema<{ [key in Unwrap<Keys>]: Unwrap<Values> }>} */ export class $Record extends Schema { /** * @param {Keys} keys * @param {Values} values */ constructor (keys, values) { super() this.shape = { keys, values } } /** * @param {any} o * @param {ValidationError} err * @return {o is { [key in Unwrap<Keys>]: Unwrap<Values> }} */ check (o, err) { if (o === null || !object.isObject(o)) { /* c8 ignore next */ err?.extend(null, 'Record', o === null ? 'null' : typeof o) return false } return object.every(o, (vv, vk) => { const ck = this.shape.keys.check(vk, err) /* c8 ignore next */ !ck && err?.extend(vk + '', 'Record', typeof o, ck ? 'Key doesn\'t match schema' : 'Value doesn\'t match value') return ck && this.shape.values.check(vv, err) }) } } /** * @template {Schema<string|number|symbol>} Keys * @template {Schema<any>} Values * @param {Keys} keys * @param {Values} values * @return {CastToSchema<$Record<Keys,Values>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $record = (keys, values) => new $Record(keys, values) export const $$record = /* @__PURE__ */$type('s:$Record', $Record) /** * @template {Schema<any>[]} S * @extends {Schema<{ [Key in keyof S]: S[Key] extends Schema<infer Type> ? Type : never }>} */ export class $Tuple extends Schema { /** * @param {S} shape */ constructor (shape) { super() this.shape = shape } /** * @param {any} o * @param {ValidationError} err * @return {o is { [K in keyof S]: S[K] extends Schema<infer Type> ? Type : never }} */ check (o, err) { return o != null && object.every(this.shape, (vv, vk) => { const c = /** @type {Schema<any>} */ (vv).check(o[vk], err) /* c8 ignore next */ !c && err?.extend(vk.toString(), 'Tuple', typeof vv) return c }) } } /** * @template {Array<Schema<any>>} T * @param {T} def * @return {CastToSchema<$Tuple<T>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $tuple = (...def) => new $Tuple(def) export const $$tuple = /* @__PURE__ */$type('s:$Tuple', $Tuple) /** * @template {Schema<any>} S * @extends {Schema<Array<S extends Schema<infer T> ? T : never>>} */ export class $Array extends Schema { /** * @param {Array<S>} v */ constructor (v) { super() /** * @type {Schema<S extends Schema<infer T> ? T : never>} */ this.shape = v.length === 1 ? v[0] : new $Union(v) } /** * @param {any} o * @param {ValidationError} [err] * @return {o is Array<S extends Schema<infer T> ? T : never>} o */ check (o, err) { const c = arr.isArray(o) && arr.every(o, oi => this.shape.check(oi)) /* c8 ignore next */ !c && err?.extend(null, 'Array', '') return c } } /** * @template {Array<Schema<any>>} T * @param {T} def * @return {Schema<Array<T extends Array<Schema<infer S>> ? S : never>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $array = (...def) => new $Array(def) export const $$array = /* @__PURE__ */$type('s:$Array', $Array) /** * @type {Schema<Array<any>>} */ export const $arrayAny = /* @__PURE__ */$custom(o => arr.isArray(o)) /** * @template T * @extends {Schema<T>} */ export class $InstanceOf extends Schema { /** * @param {new (...args:any) => T} constructor * @param {((o:T) => boolean)|null} check */ constructor (constructor, check) { super() this.shape = constructor this._c = check } /** * @param {any} o * @param {ValidationError} err * @return {o is T} */ check (o, err) { const c = o instanceof this.shape && (this._c == null || this._c(o)) /* c8 ignore next */ !c && err?.extend(null, this.shape.name, o?.constructor.name) return c } } /** * @template T * @param {new (...args:any) => T} c * @param {((o:T) => boolean)|null} check * @return {Schema<T>} */ /* @__NO_SIDE_EFFECTS__ */ export const $instanceOf = (c, check = null) => new $InstanceOf(c, check) export const $$instanceOf = /* @__PURE__ */$type('s:$InstanceOf', $InstanceOf) export const $$schema = /** @type {Schema<Schema<any>>} */ (Schema.prototype.$type = $custom(o => o?.isSchema === schemaSymbol)) /** * @template {Schema<any>[]} Args * @typedef {(...args:UnwrapArray<TuplePop<Args>>)=>Unwrap<TupleLast<Args>>} _LArgsToLambdaDef */ /** * @template {Array<Schema<any>>} Args * @extends {Schema<_LArgsToLambdaDef<Args>>} */ export class $Lambda extends Schema { /** * @param {Args} args */ constructor (args) { super() this.len = args.length - 1 this.args = $tuple(...args.slice(-1)) this.res = args[this.len] } /** * @param {any} f * @param {ValidationError} err * @return {f is _LArgsToLambdaDef<Args>} */ check (f, err) { const c = f.constructor === Function && f.length <= this.len /* c8 ignore next */ !c && err?.extend(null, 'function', typeof f) return c } } /** * @template {Schema<any>[]} Args * @param {Args} args * @return {Schema<(...args:UnwrapArray<TuplePop<Args>>)=>Unwrap<TupleLast<Args>>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $lambda = (...args) => new $Lambda(args.length > 0 ? args : [$void]) export const $$lambda = /* @__PURE__ */$type('s:$Lambda', $Lambda) /** * @type {Schema<Function>} */ export const $function = /* @__PURE__ */$custom(o => typeof o === 'function') /** * @template {Array<Schema<any>>} T * @extends {Schema<Intersect<UnwrapArray<T>>>} */ export class $Intersection extends Schema { /** * @param {T} v */ constructor (v) { super() /** * @type {T} */ this.shape = v } /** * @param {any} o * @param {ValidationError} [err] * @return {o is Intersect<UnwrapArray<T>>} */ check (o, err) { // @ts-ignore const c = arr.every(this.shape, check => check.check(o, err)) /* c8 ignore next */ !c && err?.extend(null, 'Intersectinon', typeof o) return c } } /** * @template {Schema<any>[]} T * @param {T} def * @return {CastToSchema<$Intersection<T>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $intersect = (...def) => new $Intersection(def) export const $$intersect = /* @__PURE__ */$type('s:Intersection', $Intersection) /** * @template S * @extends {Schema<S>} */ export class $Union extends Schema { /** * @param {Array<Schema<S>>} v */ constructor (v) { super() this.shape = v } /** * @param {any} o * @param {ValidationError} [err] * @return {o is S} */ check (o, err) { const c = arr.some(this.shape, (vv) => vv.check(o, err)) err?.extend(null, 'Union', typeof o) return c } } /** * @template {Array<any>} T * @param {T} schemas * @return {CastToSchema<$Union<ReadSchemaUnwrapped<T>>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $union = (...schemas) => schemas.findIndex($s => $$union.check($s)) >= 0 ? $union(...schemas.map($).map($s => $$union.check($s) ? $s.shape : [$s]).flat(1)) : (schemas.length === 1 ? schemas[0] : new $Union(schemas.map($))) export const $$union = /** @type {Schema<$Union<any>>} */ (/* @__PURE__ */$type('s:$Union', $Union)) /** * @template {any} IN * @typedef {[Extract<IN,Schema<any>>,Extract<IN,string|number|boolean|null>,Extract<IN,new (...args:any[])=>any>,Extract<IN,any[]>,Extract<Exclude<IN,Schema<any>|string|number|boolean|null|(new (...args:any[])=>any)|any[]>,object>] extends [infer Schemas, infer Primitives, infer Constructors, infer Arrs, infer Obj] * ? ( * (Schemas extends Schema<infer S> ? S : never) * | Primitives * | (Constructors extends new (...args:any[])=>any ? InstanceType<Constructors> : never) * | (Arrs extends any[] ? { [K in keyof Arrs]: ReadSchemaUnwrapped<Arrs[K]> }[keyof Arrs & number] : never) * | (Obj extends object ? _ObjectDefToSchema<{[K in keyof Obj]:Schema<ReadSchemaUnwrapped<Obj[K]>>}> : never)) * : never * } ReadSchemaUnwrapped */ /** * @template {any} IN * @typedef {Schema<ReadSchemaUnwrapped<IN>>} ReadSchema */ /** * @template IN * @param {IN} o * @return {Schema<import('./ts.js').TypeIsAny<IN,any,ReadSchemaUnwrapped<IN>>>} */ /* @__NO_SIDE_EFFECTS__ */ export const $ = o => { if ($$schema.check(o)) { return /** @type {any} */ (o) } else if ($objectAny.check(o)) { /** * @type {any} */ const o2 = {} for (const k in o) { o2[k] = $(o[k]) } return /** @type {any} */ ($object(o2)) } else if ($arrayAny.check(o)) { return /** @type {any} */ ($union(...o.map($))) } else if ($primitive.check(o)) { return /** @type {any} */ ($literal(o)) } else if ($function.check(o)) { return /** @type {any} */ ($constructedBy(/** @type {any} */ (o))) } /* c8 ignore next 2 */ error.unexpectedCase() } /* c8 ignore start */ /** * Assert that a variable is of this specific type. * @type {<T>(o:any,schema:Schema<T>) => asserts o is T} */ /* @__NO_SIDE_EFFECTS__ */ export const assert = (o, schema) => { const err = new ValidationError() if (!schema.check(o, err)) { throw error.create(`Expected value to be of type ${schema.constructor.name}.\n${err.toString()}`) } } /* c8 ignore end */ /** * Assert that a variable is of this specific type. * @type {<T>(o:any,schema:Schema<T>) => asserts o is T} */ /* @__NO_SIDE_EFFECTS__ */ export const assertNoCheck = (_o, _schema) => { } /* @__NO_SIDE_EFFECTS__ */ const _t = () => true /** * @type {Schema<any>} */ export const $any = /* @__PURE__ */$custom(_t) export const $$any = /** @type {Schema<Schema<any>>} */ (/* @__PURE__ */$type('s:$any', $any)) /** * @type {Schema<bigint>} */ export const $bigint = /* @__PURE__ */$custom(o => typeof o === 'bigint') export const $$bigint = /** @type {Schema<Schema<BigInt>>} */ (/* @__PURE__ */$type('s:$bigint', $bigint)) /** * @type {Schema<symbol>} */ export const $symbol = /* @__PURE__ */$custom(o => typeof o === 'symbol') export const $$symbol = /** @type {Schema<Schema<Symbol>>} */ (/* @__PURE__ */$type('s:$symbol', $symbol)) /** * @type {Schema<number>} */ export const $number = /* @__PURE__ */$custom(o => typeof o === 'number') export const $$number = /** @type {Schema<Schema<number>>} */ (/* @__PURE__ */$type('s:$number', $number)) /** * A number without fractional component in the safe integer range (`Number.isSafeInteger`, * i.e. `|n| <= 2^53 - 1`). The unwrapped type is still `number` - JS has no separate integer type. * * The range restriction guarantees that every `$int` is exactly representable as a 64-bit * signed integer (`BigInt(n)` is lossless, databases can store it in an `int64` / `BIGINT` column). * Values like `1e20` are integers to `Number.isInteger`, but they are neither exact (every float * beyond 2^53 rounds to a multiple of a power of two) nor storable in 64 bits, so they are * rejected. Use `$number` for unbounded floats and `$bigint` for arbitrary-precision integers. * * @type {Schema<number>} */ export const $int = /* @__PURE__ */$custom(o => number.isSafeInteger(o)) export const $$int = /** @type {Schema<Schema<number>>} */ (/* @__PURE__ */$type('s:$int', $int)) /** * An unsigned integer: a `$int` that is `>= 0` (so `0 <= n <= 2^53 - 1`). The unwrapped type is * still `number` - JS has no separate integer type. * * Like `$int`, every `$uint` is exactly representable as a 64-bit integer (signed or unsigned). * See `$int` for why the range is restricted. * * @type {Schema<number>} */ export const $uint = /* @__PURE__ */$custom(o => number.isSafeInteger(o) && o >= 0) export const $$uint = /** @type {Schema<Schema<number>>} */ (/* @__PURE__ */$type('s:$uint', $uint)) /** * @type {Schema<string>} */ export const $string = /* @__PURE__ */$custom(o => typeof o === 'string') export const $$string = /** @type {Schema<Schema<string>>} */ (/* @__PURE__ */$type('s:$string', $string)) /** * @type {Schema<boolean>} */ export const $boolean = /* @__PURE__ */$custom(o => typeof o === 'boolean') export const $$boolean = /** @type {Schema<Schema<Boolean>>} */ (/* @__PURE__ */$type('s:$boolean', $boolean)) /** * `$true`, `$false`, `$undefined` & `$null` are plain `$Literal`s (no own `$type`), so the generic * `$$literal` handling covers them in `random`, `coerce` & error messages. * * @type {Schema<true>} */ export const $true = /* @__PURE__ */$literal(true) /** * @type {Schema<false>} */ export const $false = /* @__PURE__ */$literal(false) /** * @type {Schema<undefined>} */ export const $undefined = /* @__PURE__ */$literal(undefined) /** * @type {Schema<void>} */ export const $void = $undefined export const $null = $literal(null) export const $uint8Array = $constructedBy(Uint8Array) export const $$uint8Array = /** @type {Schema<Schema<Uint8Array>>} */ ($type('s:$uint8Array', $uint8Array)) export const $promise = $constructedBy(Promise) export const $$promise = /** @type {Schema<Schema<Uint8Array>>} */ ($type('s:$promise', $promise)) /** * Primitive JavaScript types: immutable, non-object value that is stored and compared directly by * its value rather than by reference. * * @type {Schema<Primitive>} */ export const $primitive = $union($number, $string, $null, $undefined, $bigint, $boolean, $symbol) /** * @typedef {JSON[]} JSONArray */ /** * @typedef {Primitive|JSONArray|{ [key:string]:JSON }} JSON */ /** * @type {Schema<null|number|string|boolean|JSON[]|{[key:string]:JSON}>} */ export const $json = /* @__PURE__ */(() => { const $jsonArr = /** @type {$Array<typeof $any>} */ ($array($any)) const $jsonRecord = /** @type {$Record<typeof $string,typeof $any>} */ ($record($string, $any)) const $json = $union($number, $string, $null, $boolean, $jsonArr, $jsonRecord) $jsonArr.shape = $json $jsonRecord.shape.values = $json return $json })() /** * @template In * @template Out * @typedef {{ if: Schema<In>, h: (o:In,state?:any)=>Out }} Pattern */ /** * @template {Pattern<any,any>} Ps * @template S * @typedef {import('./ts.js').UnionToIntersection<Ps extends Pattern<infer In, infer Out> ? [S] extends [undefined] ? (o: In) => Out : (o: In, state: S) => Out : never> * & ([S] extends [undefined] * ? (o: Ps extends Pattern<infer In, any> ? In : never) => (Ps extends Pattern<any, infer Out> ? Out : never) * : (o: Ps extends Pattern<infer In, any> ? In : never, state: S) => (Ps extends Pattern<any, infer Out> ? Out : never)) * } PatternMatcherFn */ const unhandledPatternError = 'Unhandled pattern' /** * @todo move this to separate library * @template {any} [State=undefined] * @template {Pattern<any,any>} [Patterns=never] */ export class PatternMatcher { /** * @param {Schema<State>} [$state] */ constructor ($state) { /** * @type {Array<Patterns>} */ this.patterns = [] this.$state = $state } /** * @template P * @template R * @param {P} pattern * @param {(o:NoInfer<ReadSchemaUnwrapped<P>>,s:State)=>R} handler * @return {PatternMatcher<State,Patterns|Pattern<ReadSchemaUnwrapped<P>,R>>} */ if (pattern, handler) { // @ts-ignore this.patterns.push({ if: $(pattern), h: handler }) // @ts-ignore return this } /** * @template R * @param {(o:any,s:State)=>R} h */ else (h) { return this.if($any, h) } /** * @param {string} errorMessage */ error (errorMessage = unhandledPatternError) { return this.if($any, () => { throw error.create(errorMessage) }).done() } /** * @return {PatternMatcherFn<Patterns, State>} */ done () { // @ts-ignore return /** @type {any} */ (o, s) => { for (let i = 0; i < this.patterns.length; i++) { const p = this.patterns[i] if (p.if.check(o)) { // @ts-ignore return p.h(o, s) } } throw error.create(unhandledPatternError) } } } /** * @template [State=undefined] * @param {State} [state] * @return {PatternMatcher<State extends undefined ? undefined : ReadSchemaUnwrapped<State>>} */ /* @__NO_SIDE_EFFECTS__ */ export const match = state => new PatternMatcher(/** @type {any} */ (state)) /** * Helper function to generate a (non-exhaustive) sample set from a gives schema. * * @type {<T>(o:T,opts:any)=>ReadSchemaUnwrapped<T>} */ const _random = /* @__PURE__ */ (() => match({ gen: /** @type {Schema<prng.PRNG>} */ ($any), fallback: $lambda($any, $any, $any).optional }) .if($$number, (_o, { gen }) => prng.oneOf(gen, [-1, 0, 1, prng.int53(gen, number.MIN_SAFE_INTEGER, number.MAX_SAFE_INTEGER)])) .if($$int, (_o, { gen }) => prng.oneOf(gen, [-1, 0, 1, prng.int53(gen, number.MIN_SAFE_INTEGER, number.MAX_SAFE_INTEGER)])) .if($$uint, (_o, { gen }) => prng.oneOf(gen, [0, 1, prng.int53(gen, 0, number.MAX_SAFE_INTEGER)])) .if($$string, (_o, { gen }) => prng.word(gen)) .if($$boolean, (_o, { gen }) => prng.bool(gen)) .if($$bigint, (_o, { gen }) => BigInt(prng.int53(gen, number.MIN_SAFE_INTEGER, number.MAX_SAFE_INTEGER))) .if($$union, (o, opts) => _random(prng.oneOf(opts.gen, o.shape), opts)) .if($$object, (o, opts) => { /** * @type {any} */ const res = {} for (const k in o.shape) { let prop = o.shape[k] if ($$optional.check(prop)) { if (prng.bool(opts.gen)) { continue } prop = prop.shape } res[k] = _random(prop, opts) } return res }) .if($$array, (o, opts) => { /** * @type {Array<any>} */ const arr = [] const n = prng.int32(opts.gen, 0, 42) for (let i = 0; i < n; i++) { arr.push(_random(o.shape, opts)) } return arr }) .if($$literal, (o, { gen }) => { return prng.oneOf(gen, o.shape) }) .if($$lambda, (o, opts) => { const res = _random(o.res, opts) return () => res }) .if($$any, (_o, opts) => _random(prng.oneOf(opts.gen, [ $number, $string, $null, $undefined, $bigint, $boolean, $array($number), $record($union('a', 'b', 'c'), $number) ]), opts)) .if($$record, (o, opts) => { /** * @type {any} */ const res = {} const keysN = prng.int53(opts.gen, 0, 3) for (let i = 0; i < keysN; i++) { const key = _random(o.shape.keys, opts) const val = _random(o.shape.values, opts) res[key] = val } return res }) .else((o, { gen, fallback }) => { if (fallback == null) { throw new Error('No random generator for schema provided') } return fallback(gen, o) }) .done())() /** * @template S * @param {prng.PRNG} gen * @param {S} schema * @param {(gen:prng.PRNG,o:Schema<any>)=>any} [fallback] * @return {ReadSchemaUnwrapped<S>} */ /* @__NO_SIDE_EFFECTS__ */ export const random = (gen, schema, fallback) => /* @__PURE__ */_random($(schema), { gen, fallback }) /** * Carries the reason of a failed coercion out of the recursion. * * @typedef {{ err: string }} _CoerceCtx */ /** * A compiled coercer. Returns the coerced value, or `_failed` after writing the reason to * `ctx.err`. * * @typedef {(o:any, path:string, ctx:_CoerceCtx) => any} _Coercer */ const _failed = Symbol('schema:coercion failed') const _bigintRegex = /^[+-]?\d+$/ const _base64Regex = /^[A-Za-z0-9+/]+={0,2}$/ /** * Render a value for an error message. Objects are only rendered by kind - printing them adds * noise (and `String` throws on null-prototype objects). * * @param {any} o * @return {string} */ /* @__NO_SIDE_EFFECTS__ */ const _show = o => typeof o === 'string' ? JSON.stringify(o) : (o !== null && object.isObject(o) ? (arr.isArray(o) ? 'array' : 'object') : String(o)) /** * @param {_CoerceCtx} ctx * @param {string} path * @param {any} o * @param {string} expected * @return {typeof _failed} */ const _fail = (ctx, path, o, expected) => { ctx.err = `${path === '' ? '' : `[${path}] `}${_show(o)} doesn't match ${expected}` return _failed } /** * @param {string} path * @param {string} key */ /* @__NO_SIDE_EFFECTS__ */ const _prop = (path, key) => path === '' ? key : `${path}.${key}` /** * @param {string} path * @param {number} i */ /* @__NO_SIDE_EFFECTS__ */ const _idx = (path, i) => path === '' ? `${i}` : `${path}[${i}]` /** * `$$x.check` is a type predicate. Dispatching through this indirection keeps `$s` from being * narrowed to `never` in the negative branches of the coercer dispatch. * * @param {Schema<any>} $meta * @param {Schema<any>} $s * @return {boolean} */ const _isMeta = ($meta, $s) => $meta.check($s) /** * A short, human readable name for a schema - used in coercion error messages. * * Only `$union` & `$optional` are descended into (`$union` flattens nested unions, so this * terminates). Container shapes are *not* descended into - that's what makes this safe on * self-referential schemas like `$json`. * * @param {Schema<any>} $s * @return {string} */ const _nameOf = $s => { const shape = /** @type {any} */ ($s).shape if (_isMeta($$any, $s)) return 'any' if (_isMeta($$string, $s)) return 'string' if (_isMeta($$number, $s)) return 'number' if (_isMeta($$int, $s)) return 'int' if (_isMeta($$uint, $s)) return 'uint' if (_isMeta($$bigint, $s)) return 'bigint' if (_isMeta($$boolean, $s)) return 'boolean' if (_isMeta($$symbol, $s)) return 'symbol' if (_isMeta($$literal, $s)) { return /** @type {Array<Primitive>} */ (shape).map(l => String(l)).join(' | ') } if (_isMeta($$optional, $s)) return `${_nameOf(shape)} | undefined` if (_isMeta($$union, $s)) return /** @type {Array<Schema<any>>} */ (shape).map(_nameOf).join(' | ') if (_isMeta($$object, $s)) return 'object' if (_isMeta($$record, $s)) return 'record' if (_isMeta($$tuple, $s)) return 'tuple' if (_isMeta($$array, $s)) return 'array' // `instanceof` instead of the meta schemas: `$uint8Array` & `$promise` overwrite their inherited // `$type` with a more specific one, so `_isMeta($$constructedBy, ..)` doesn't hold for them if ($s instanceof $ConstructedBy || $s instanceof $InstanceOf) return shape.name return $s.constructor.name } /** * Compile a coercer for `$s`. Dispatches on the meta schemas (`$$string`, `$$object`, ..) which are * an exact identity check. * * @param {Schema<any>} $s * @param {Map<Schema<any>,_Coercer>} cache * @return {_Coercer} */ const _createCoercer = ($s, cache) => { // `shape` is only defined on the subclasses - see the comment on `Schema` const shape = /** @type {any} */ ($s).shape const expected = _nameOf($s) if (_isMeta($$any, $s)) { return o => o } if (_isMeta($$string, $s)) { return (o, path, ctx) => { const t = typeof o return t === 'string' ? o : (t === 'number' || t === 'bigint' || t === 'boolean' ? o + '' : _fail(ctx, path, o, expected)) } } if (_isMeta($$number, $s)) { return (o, path, ctx) => { const t = typeof o if (t === 'number') return o if (t === 'boolean' || t === 'bigint' || (t === 'string' && o.trim() !== '')) { const n = Number(o) if (!number.isNaN(n)) return n } return _fail(ctx, path, o, expected) } } if (_isMeta($$int, $s) || _isMeta($$uint, $s)) { const min = _isMeta($$uint, $s) ? 0 : -Infinity return (o, path, ctx) => { if (number.isSafeInteger(o) && o >= min) return o const t = typeof o if (t === 'boolean' || t === 'bigint' || (t === 'string' && o.trim() !== '')) { const n = Number(o) if (number.isSafeInteger(n) && n >= min) return n } return _fail(ctx, path, o, expected) } } if (_isMeta($$bigint, $s)) { return (o, path, ctx) => { const t = typeof o return t === 'bigint' ? o : ((t === 'number' && number.isInteger(o)) || (t === 'string' && _bigintRegex.test(o)) ? BigInt(o) : _fail(ctx, path, o, expected)) } } if (_isMeta($$boolean, $s)) { return (o, path, ctx) => typeof o === 'boolean' ? o : (o === 'true' ? true : (o === 'false' ? false : _fail(ctx, path, o, expected))) } if (_isMeta($$uint8Array, $s)) { // the format is checked here (rather than left to the decoder) because the `buffer.fromBase64` // backends disagree on invalid input: `Buffer` silently ignores unexpected characters, `atob` & // `Uint8Array.fromBase64` throw return (o, path, ctx) => { if ($uint8Array.check(o)) return o if (typeof o !== 'string' || !_base64Regex.test(o)) return _fail(ctx, path, o, expected) // base64 encodes 3 bytes as 4 characters. The '=' padding is optional, but when it is there // the length has to be a multiple of 4 - and without it the last chunk must not be a single // character, which carries no full byte. const rest = o.length % 4 return (o.endsWith('=') ? rest === 0 : rest !== 1) ? buffer.fromBase64(o) : _fail(ctx, path, o, expected) } } if (_isMeta($$literal, $s)) { const literals = /** @type {Array<Primitive>} */ (shape) return (o, path, ctx) => { if (arr.some(literals, l => l === o)) return o if (typeof o === 'string') { const i = literals.findIndex(l => String(l) === o) if (i >= 0) return literals[i] } return _fail(ctx, path, o, expected) } } if (_isMeta($$optional, $s)) { const c = _buildCoercer(shape, cache) return (o, path, ctx) => o === undefined ? undefined : c(o, path, ctx) } if (_isMeta($$union, $s)) { const members = /** @type {Array<Schema<any>>} */ (shape) const cs = members.map($m => _buildCoercer($m, cache)) return (o, path, ctx) => { // a value that already matches one of the members is never converted if (arr.some(members, $m => $m.check(o))) return o for (let i = 0; i < cs.length; i++) { const v = cs[i](o, path, ctx) if (v !== _failed) return v } return _fail(ctx, path, o, expected) } } if (_isMeta($$object, $s)) { const isPartial = /** @type {any} */ ($s)._isPartial /** * @type {Array<{ key: string, skippable: boolean, c: _Coercer }>} */ const props = [] for (const key in shape) { props.push({ key, skippable: isPartial || $$optional.check(shape[key]), c: _buildCoercer(shape[key], cache) }) } return (o, path, ctx) => { if (o === null || !object.isObject(o)) return _fail(ctx, path, o, expected) let changed = false /** * @type {any} */ const res = {} for (let i = 0; i < props.length; i++) { const { key, skippable, c } = props[i] if (skippable && !object.hasProperty(o, key)) continue const v = c(o[key], _prop(path, key), ctx) if (v === _failed) return _failed changed = changed || v !== o[key] res[key] = v } // unknown props are preserved - `$Object` accepts them as well return changed ? object.assign({}, o, res) : o } } if (_isMeta($$record, $s)) { const ckey = _buildCoercer(shape.keys, cache) const cval = _buildCoercer(shape.values, cache) return (o, path, ctx) => { if (o === null || !object.isObject(o)) return _fail(ctx, path, o, expected) let changed = false /** * @type {any} */ const res = {} for (const key in o) { const p = _prop(path, key) // keys are coerced only to validate them - the property name stays a string either way if (ckey(key, p, ctx) === _failed) return _failed const v = cval(o[key], p, ctx) if (v === _failed) return _failed changed = changed || v !== o[key] res[key] = v } return changed ? res : o } } if (_isMeta($$tuple, $s)) { const cs = /** @type {Array<Schema<any>>} */ (shape).map($m => _buildCoercer($m, cache)) return (o, path, ctx) => { if (!arr.isArray(o)) return _fail(ctx, path, o, expected) let changed = false // `$Tuple` doesn't constrain the length - additional items are preserved const res = o.slice() for (let i = 0; i < cs.length; i++) { const v = cs[i](o[i], _idx(path, i), ctx) if (v === _failed) return _failed changed = changed || v !== o[i] res[i] = v } return changed ? res : o } } if (_isMeta($$array, $s)) { const c = _buildCoercer(shape, cache) return (o, path, ctx) => { if (!arr.isArray(o)) return _fail(ctx, path, o, expected) let changed = false const res = new Array(o.length) for (let i = 0; i < o.length; i++) { const v = c(o[i], _idx(path, i), ctx) if (v === _failed) return _failed changed = changed || v !== o[i] res[i] = v } return changed ? res : o } } // everything else ($instanceOf, $constructedBy, $custom, $lambda, $intersect, $stringTemplate, // $type, ..) has no meaningful conversion - accept the value iff it already matches return (o, path, ctx) => $s.check(o) ? o : _fail(ctx, path, o, expected) } /** * Compile with memoization. The coercer is registered *before* recursing so that self-referential * schemas (e.g. `$json`) terminate. * * @param {Schema<any>} $s * @param {Map<Schema<any>,_Coercer>} cache * @return {_Coercer} */ const _buildCoercer = ($s, cache) => { const cached = cache.get($s) if (cached != null) return cached // `self` is a box because the registered forwarder must close over a coercer that doesn't exist // yet /** * @type {Array<_Coercer>} */ const self = [] cache.set($s, (o, path, ctx) => self[0](o, path, ctx)) self.push(_createCoercer($s, cache)) return self[0] } /** * Compile a coercion function for `schema`. * * Unlike `check` (which only validates) a coercion converts values that don't match yet - `'42'` * becomes `42` for `$number`, `'true'` becomes `true` for `$boolean`. Values that already match * are returned untouched (including object identity). Containers (`$object`, `$array`, `$record`, * `$tuple`, `$union`, `$optional`) are coerced recursively. `$uint8Array` accepts a base64 string * (padding optional) - the inverse of `buffer.toBase64`. Schemas without a meaningful conversion * (`$instanceOf`, `$custom`, ..) simply have to match. * * Failures are returned instead of thrown - this is the only api in lib0 that reports errors as a * value. * * @example * const readConfig = coerce($object({ port: $number, dev: $boolean })) * readConfig({ port: '8080', dev: 'true' }) // => { err: null, result: { port: 8080, dev: true } } * readConfig({ port: 'x', dev: 'true' }) // => { err: '[port] "x" doesn\'t match number', result: null } * * @template {Schema<any>} S * @param {S} schema * @return {(o:any) => { err: string, result: null } | { err: null, result: Unwrap<S> }} */ /* @__NO_SIDE_EFFECTS__ */ export const coerce = schema => { const c = _buildCoercer(schema, new Map()) return o => { const ctx = { err: '' } const res = c(o, '', ctx) return res === _failed ? { err: ctx.err, result: null } : { err: null, result: res } } }