Vendor Cordis framework packages as source

cordis 4.0.0-rc.6, plugin-loader, -include, -group, -timer, -hmr,
-logger-console, cosmokit 1.8.1, schemastery 3.18.0 — copied from the
cordis-workspace checkout, flattened under vendor/, original npm names,
private: true. vendor/README.md is the manifest: upstream repos +
commit SHAs, local-modification log, sync procedure.

Local modification: hmr's locale YAML imports and .i18n() call removed
(avoids a runtime YAML import hook we don't vendor).
This commit is contained in:
Tianyi Cui
2026-06-11 10:53:32 +08:00
parent ae2e08b4d6
commit 72688a3888
69 changed files with 6659 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+389
View File
@@ -0,0 +1,389 @@
# Schemastery
[![Codecov](https://img.shields.io/codecov/c/github/shigma/schemastery?style=flat-square)](https://codecov.io/gh/shigma/schemastery)
[![downloads](https://img.shields.io/npm/dm/schemastery?style=flat-square)](https://www.npmjs.com/package/schemastery)
[![npm](https://img.shields.io/npm/v/schemastery?style=flat-square)](https://www.npmjs.com/package/schemastery)
[![GitHub](https://img.shields.io/github/license/shigma/schemastery?style=flat-square)](https://github.com/shigma/schemastery/blob/master/LICENSE)
Type Driven Schema Validator.
## Features
- **Lightweight.** Much smaller than other validation libraries.
- **Easy to use.** You can use any schema as a function or constructor directly.
- **Powerful.** Schemastery supports some advanced types such as `union`, `intersect` and `transform`.
- **Extensible.** You can create your own schema types via `Schema.extend()`.
- **Serializable.** Schema objects can be serialized into JSON and then be hydrated in another environment.
## Basic Examples
### use as validator (JavaScript)
```js
const Schema = require('schemastery')
const validate = Schema.number().default(10)
validate(0) // 0
validate(null) // 10
validate('') // TypeError
```
### use as constructor (TypeScript)
```ts
import Schema from 'schemastery'
interface Config {
foo: Record<string, string>
bar: string[]
}
const Config = Schema.object({
foo: Schema.dict(Schema.string()).default({}),
bar: Schema.array(Schema.string()).default([]),
})
// config is an instance of Config
// in this case, that is { foo: {}, bar: [] }
const config = new Config()
```
## General Types
### Schema.any()
Assert that the value is of any type.
```js
const validate = Schema.any()
validate() // undefined
validate(0) // 0
validate({}) // {}
```
### Schema.never()
Assert that the value is nullable.
```js
const validate = Schema.never()
validate() // undefined
validate(0) // TypeError
validate({}) // TypeError
```
### 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()
Assert that the value is a number.
```js
const validate = Schema.number()
validate() // undefined
validate(1) // 1
validate('') // TypeError
```
### Schema.string()
Assert that the value is a string.
```js
const validate = Schema.string()
validate() // undefined
validate(0) // TypeError
validate('foo') // 'foo'
```
### Schema.boolean()
Assert that the value is a boolean.
```js
const validate = Schema.boolean()
validate() // undefined
validate(0) // TypeError
validate(true) // true
```
### 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(inner)
Assert that the value is an array of `inner`. The default value will be `[]` if not specified.
```js
const validate = Schema.array(Schema.number())
validate() // []
validate(0) // TypeError
validate([0, 1]) // [0, 1]
validate([0, '1']) // TypeError
```
### Schema.dict(inner)
Assert that the value is a dictionary of `inner`. The default value will be `{}` if not specified.
```js
const validate = Schema.dict(Schema.number())
validate() // {}
validate(0) // TypeError
validate({ a: 0, b: 1 }) // { a: 0, b: 1 }
validate({ a: 0, b: '1' }) // TypeError
```
### Schema.tuple(list)
Assert that the value is a tuple whose each element is of corresponding subtype. The default value will be `[]` if not specified.
```js
const validate = Schema.tuple([
Schema.number(),
Schema.string(),
])
validate() // []
validate([0]) // { a: 0 }
validate([0, 1]) // TypeError
validate([0, '1']) // [0, '1']
```
### Schema.object(dict)
Assert that the value is an object whose each property is of corresponding subtype. The default value will be `{}` if not specified.
```js
const validate = Schema.object({
a: Schema.number(),
b: Schema.string(),
})
validate() // {}
validate({ a: 0 }) // { a: 0 }
validate({ a: 0, b: 1 }) // TypeError
validate({ a: 0, b: '1' }) // { a: 0, b: '1' }
```
### Schema.union(list)
Assert that the value is one of the specified types.
```js
const validate = Schema.union([
Schema.number(),
Schema.string(),
])
validate() // undefined
validate(0) // 0
validate('1') // '1'
validate(true) // TypeError
```
### Schema.intersect(list)
Assert that the value should match each specified type.
```js
const validate = Schema.intersect([
Schema.object({ a: Schema.string().required() }),
Schema.object({ b: Schema.number().default(0) }),
])
validate() // TypeError
validate({ a: '' }) // { a: '', b: 0 }
validate({ a: '', b: 1 }) // { a: '', b: 1 }
validate({ a: '', b: '2' }) // TypeError
```
### Schema.transform(inner, callback)
Assert that the value is of the specified subtype and then transformed by `callback`.
```js
const validate = Schema.transform(Schema.number().default(0), n => n + 1)
validate() // 1
validate('0') // TypeError
validate(10) // 11
```
## Instance Methods
Note: `default` and `required` are mutually exclusive.
### 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.
### schema.simplify(value)
Normalize a value by removing parts that are equal to schema defaults. This is
useful when storing user configuration and keeping persisted files compact.
```js
const Config = Schema.object({
foo: Schema.string().default(''),
bar: Schema.number().default(0),
})
Config.simplify({ foo: '', bar: 1 }) // { bar: 1 }
```
## Validation Options
All schemas are callable. The second argument accepts validation options:
```js
const Config = Schema.object({
foo: Schema.number(),
})
Config({ foo: '1' }, { autofix: true }) // {}
```
- `autofix`: remove invalid object properties where possible.
- `ignore`: skip validation for selected values and schema nodes.
- `path`: provide an initial path for nested validation errors.
## Shorthand Syntax
Some shorthand syntax is available for inner types.
- `undefined` -> `Schema.any()`
- `String` -> `Schema.string()`
- `Number` -> `Schema.number()`
- `Boolean` -> `Schema.boolean()`
- `1` -> `Schema.const(1)` (only for primitive types)
- `Date` -> `Schema.is(Date)`
```js
Schema.array(String) // Schema.array(Schema.string())
Schema.dict(RegExp) // Schema.dict(Schema.is(RegExp))
Schema.union([1, 2]) // Schema.union([Schema.const(1), Schema.const(2)])
```
You can also use `Schema.from()` to get the inferred schema from a shorthand value.
```js
Schema.from() // Schema.any()
Schema.from(Date) // Schema.is(Date)
Schema.from('foo') // Schema.const('foo')
```
## 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(Number),
Schema.transform(Number, n => [n]),
]).default([])
Listable() // []
Listable(0) // [0]
Listable([1, 2]) // [1, 2]
```
### Alias
```js
const Config = Schema.dict(Number, Schema.union([
'foo',
Schema.transform('bar', () => 'foo'),
]))
Config({ foo: 1 }) // { foo: 1 }
Config({ bar: 2 }) // { foo: 2 }
Config({ bar: '3' }) // TypeError
```
## Extensibility
Custom schema types are registered with `Schema.extend(type, resolve)`. A
resolver receives the input value, schema node, validation options, and a strict
flag. Return `[value]` for accepted input, or `[value, adapted]` when the caller
should write an adapted value back to the source object.
```js
Schema.extend('trimmed', (data, schema, options) => {
if (typeof data !== 'string') {
throw new Schema.ValidationError(`expected string but got ${data}`, options)
}
return [data.trim()]
})
```
## Serializability
```js
const schema1 = Schema.object({
foo: Schema.string(),
bar: Schema.number(),
})
// should have the same effect as schema1
const schema2 = new Schema(JSON.parse(JSON.stringify(schema1)))
```
Schemastery also exposes the Standard Schema `~standard` property, so compatible
tools can validate values without depending on Schemastery-specific APIs.
+19
View File
@@ -0,0 +1,19 @@
{
"name": "schemastery",
"description": "Type driven schema validator",
"version": "3.18.0",
"private": true,
"main": "lib/index.cjs",
"module": "lib/index.mjs",
"types": "lib/index.d.ts",
"files": [
"lib",
"src"
],
"author": "Shigma <shigma10826@gmail.com>",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"cosmokit": "^1.8.1"
}
}
+902
View File
@@ -0,0 +1,902 @@
import { Binary, clone, deepEqual, Dict, filterKeys, isNullable, isPlainObject, pick, valueMap } from 'cosmokit'
import type { StandardSchemaV1 } from '@standard-schema/spec'
const kSchema = Symbol.for('schemastery')
const kValidationError = Symbol.for('ValidationError')
declare global {
namespace Schemastery {
/** Convert primitive constructors, constants, and existing schemas into a schema type. */
export type From<X> =
| X extends string | number | boolean ? Schema<X>
: X extends Schema ? X
: X extends typeof String ? Schema<string>
: X extends typeof Number ? Schema<number>
: X extends typeof Boolean ? Schema<boolean>
: X extends typeof Function ? Schema<Function, (...args: any[]) => any>
: X extends Constructor<infer S> ? Schema<S>
: never
type TypeS1<X> = X extends Schema<infer S, unknown> ? S : never
type Inverse<X> = X extends Schema<any, infer Y> ? (arg: Y) => void : never
/** Input type accepted by a schema-like value. */
export type TypeS<X> = TypeS1<From<X>>
/** Output type returned by a schema-like value after validation. */
export type TypeT<X> = ReturnType<From<X>>
/** Resolver callback used by custom schema types registered with `Schema.extend()`. */
export type Resolve = (data: any, schema: Schema, options: Options, strict?: boolean) => [any, any?]
/** Input type accepted by one schema in an intersection. */
export type IntersectS<X> = From<X> extends Schema<infer S, unknown> ? S : never
/** Output type returned by one schema in an intersection. */
export type IntersectT<X> = Inverse<From<X>> extends ((arg: infer T) => void) ? T : never
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]> | null } & Dict
type ObjectT<X extends Dict> = { [K in keyof X]: TypeT<X[K]> } & Dict
type Constructor<T = any> = new (...args: any[]) => T
/** Static constructor and factory methods exposed by the default `Schema` export. */
export interface Static {
<T = any>(options: Partial<Schema<T>>): Schema<T>
new <T = any>(options: Partial<Schema<T>>): Schema<T>
prototype: Schema
/** Validate a value against a schema node and return `[output, adaptedInput?]`. */
resolve: Resolve
/** Infer a schema from a primitive value, constructor, or existing schema. */
from<X = any>(source?: X): From<X>
/** Register a resolver for a custom schema `type`. */
extend(type: string, resolve: Resolve): void
/** Accept any value without validation. */
any<T = any>(): Schema<T>
/** Accept only nullable input. */
never(): Schema<never>
/** Accept exactly one constant value. */
const<const T>(value: T): Schema<T>
/** Accept strings, with optional metadata constraints added by instance methods. */
string(): Schema<string>
/** Accept numbers, with optional range and step constraints. */
number(): Schema<number>
/** Accept non-negative integer numbers. */
natural(): Schema<number>
/** Accept a number between 0 and 1 and mark it as a slider. */
percent(): Schema<number>
/** Accept booleans. */
boolean(): Schema<boolean>
/** Accept `Date` instances or parse datetime strings into `Date` objects. */
date(): Schema<string | Date, Date>
/** Accept `RegExp` instances or parse strings into regular expressions. */
regExp(flag?: string): Schema<string | RegExp, RegExp>
/** Accept binary sources and normalize them to `ArrayBufferLike`. */
arrayBuffer(): Schema<Binary.Source, ArrayBufferLike>
arrayBuffer(encoding: 'hex' | 'base64'): Schema<Binary.Source | string, ArrayBufferLike>
/** Accept a numeric bitset or string keys and normalize to a number. */
bitset<K extends string>(bits: Partial<Record<K, number>>): Schema<number | readonly K[], number>
/** Accept functions. */
function(): Schema<Function, (...args: any[]) => any>
/** Accept instances of a constructor or objects whose constructor name matches. */
is(constructor: string): Schema
is<T>(constructor: Constructor<T>): Schema<T>
/** Accept arrays whose elements match `inner`. */
array<X>(inner: X): Schema<TypeS<X>[], TypeT<X>[]>
/** Accept plain objects with values matching `inner` and optional key schema. */
dict<X, Y extends Schema<any, string> = Schema<string>>(inner: X, sKey?: Y): Schema<Dict<TypeS<X>, TypeS<Y>>, Dict<TypeT<X>, TypeT<Y>>>
/** Accept tuple arrays where each index matches the corresponding schema. */
tuple<const X extends readonly any[]>(list: X): Schema<TupleS<X>, TupleT<X>>
/** Accept plain objects whose declared properties match the schema dictionary. */
object<X extends Dict>(dict: X): Schema<ObjectS<X>, ObjectT<X>>
/** Accept values matching at least one schema in `list`. */
union<const X>(list: readonly X[]): Schema<TypeS<X>, TypeT<X>>
/** Accept values matching every schema in `list`, merging object outputs. */
intersect<const X>(list: readonly X[]): Schema<IntersectS<X>, IntersectT<X>>
/** Validate with `inner`, then convert the result with `callback`. */
transform<X, T>(inner: X, callback: (value: TypeS<X>, options: Schemastery.Options) => T, preserve?: boolean): Schema<TypeS<X>, T>
/** Defer construction of a recursive schema until validation or serialization. */
lazy<X extends Schema>(callback: () => X): X
ValidationError: typeof ValidationError
}
/** Runtime validation options shared by all schema calls. */
interface Options {
/** Remove invalid object properties instead of throwing when possible. */
autofix?: boolean
/** Skip validation for selected values and schema nodes. */
ignore?(data: any, schema: Schema): boolean
/** Path used to format nested validation errors. */
path?: (keyof any)[]
}
/** UI and validation metadata attached by schema builder methods. */
export interface Meta<T = any> {
default?: T extends {} ? Partial<T> : T
required?: boolean
disabled?: boolean
collapse?: boolean
badges?: { text: string; type: string }[]
hidden?: boolean
loose?: boolean
role?: string
extra?: any
link?: string
description?: string | Dict<string>
comment?: string
pattern?: { source: string; flags?: string }
max?: number
min?: number
step?: number
}
}
/** Callable schema instance that validates input and returns normalized output. */
interface Schemastery<S = any, T = S> {
(data?: S | null, options?: Schemastery.Options): T
new (data?: S | null, options?: Schemastery.Options): T
[kSchema]: true
uid: number
meta: Schemastery.Meta<T>
type: string
sKey?: Schema
inner?: Schema
list?: Schema[]
dict?: Dict<Schema>
bits?: Dict<number>
callback?: Function
constructor?: string | Function
builder?: Function
value?: T
refs?: Dict<Schema>
preserve?: boolean
'~standard': StandardSchemaV1.Props // <S, T>
/** Format this schema as a compact TypeScript-like type string. */
toString(inline?: boolean): string
/** Serialize this schema, preserving shared and recursive references. */
toJSON(): Schema<S, T>
/** Mark nullable input as invalid unless a default supplies a fallback. */
required(value?: boolean): Schema<S, T>
/** Hide this schema node from UI renderers. */
hidden(value?: boolean): Schema<S, T>
/** Return the default value instead of throwing when validation fails. */
loose(value?: boolean): Schema<S, T>
/** Attach a renderer role and optional role-specific metadata. */
role(text: string, extra?: any): Schema<S, T>
/** Attach an external documentation link. */
link(link: string): Schema<S, T>
/** Set the fallback value used for nullable input. */
default(value: T): Schema<S, T>
/** Attach an auxiliary comment for documentation or form UIs. */
comment(text: string): Schema<S, T>
/** Attach a localized or plain description for documentation or form UIs. */
description(text: string): Schema<S, T>
/** Mark this schema node as disabled for form UIs. */
disabled(value?: boolean): Schema<S, T>
/** Request collapsed rendering for nested form UIs. */
collapse(value?: boolean): Schema<S, T>
/** Add a deprecated badge to this schema node. */
deprecated(): Schema<S, T>
/** Add an experimental badge to this schema node. */
experimental(): Schema<S, T>
/** Require strings to match a regular expression. */
pattern(regexp: RegExp): Schema<S, T>
/** Set an inclusive maximum for numbers or collection lengths. */
max(value: number): Schema<S, T>
/** Set an inclusive minimum for numbers or collection lengths. */
min(value: number): Schema<S, T>
/** Set the numeric increment constraint. */
step(value: number): Schema<S, T>
/** Add or replace an object property schema. */
set(key: string, value: Schema): Schema<S, T>
/** Append a tuple, union, or intersection member schema. */
push(value: Schema): Schema<S, T>
/** Remove values equal to schema defaults from normalized output. */
simplify(value?: any): any
/** Return a schema clone with descriptions merged from locale messages. */
i18n(messages: Dict): Schema<S, T>
/** Attach arbitrary metadata consumed by form renderers and downstream tools. */
extra<K extends keyof Schemastery.Meta>(key: K, value: Schemastery.Meta[K]): Schema<S, T>
}
}
declare namespace globalThis {
// eslint-disable-next-line @typescript-eslint/naming-convention
export let __schemastery_index__: number
export let __schemastery_refs__: Record<number, Schema> | undefined
}
globalThis.__schemastery_index__ ??= 0
globalThis.__schemastery_refs__ = undefined
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 = {}) {
return Schema.resolve(data, schema, options)[0]
} as Schema
if (options.refs) {
const refs = valueMap(options.refs, options => new Schema(options))
const getRef = (uid: any) => refs[uid]!
for (const key in refs) {
const options = refs[key]!
options.sKey = getRef(options.sKey)
options.inner = getRef(options.inner)
options.list = options.list && options.list.map(getRef)
options.dict = options.dict && valueMap(options.dict, getRef)
}
return refs[options.uid!]
}
Object.assign(schema, options)
if (typeof schema.callback === 'string') {
try {
// eslint-disable-next-line no-new-func
schema.callback = new Function('return ' + schema.callback)()
} catch {}
}
Object.defineProperty(schema, 'uid', { value: globalThis.__schemastery_index__++ })
Object.setPrototypeOf(schema, Schema.prototype)
schema.meta ||= {}
schema.toString = schema.toString.bind(schema)
return schema
} as Schemastery.Static
Schema.prototype = Object.create(Function.prototype)
Schema.prototype[kSchema] = true
Object.defineProperty(Schema.prototype, '~standard', {
get(this: Schema) {
return {
version: 1,
vendor: 'schemastery',
validate: (value: unknown) => {
try {
return { value: Schema.resolve(value, this, {})[0] }
} catch (error) {
if (ValidationError.is(error)) {
return { issues: [{ message: error.message, path: error.options.path }] }
}
throw error
}
},
}
},
})
Schema.ValidationError = ValidationError
Schema.prototype.toJSON = function toJSON() {
if (globalThis.__schemastery_refs__) {
globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }))
return this.uid as any
}
globalThis.__schemastery_refs__ = { [this.uid]: { ...this } as Schema }
globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }))
const result = { uid: this.uid, refs: globalThis.__schemastery_refs__ }
globalThis.__schemastery_refs__ = undefined
return result
}
Schema.prototype.set = function set(key, value) {
this.dict![key] = value
return this
}
Schema.prototype.push = function push(value) {
this.list!.push(value)
return this
}
function mergeDesc(original: undefined | string | Dict<string>, messages: Dict) {
const result: Dict<string> = typeof original === 'string' ? { '': original } : { ...original }
for (const locale in messages) {
const value = messages[locale]
if (value?.$description || value?.$desc) {
result[locale] = value.$description || value.$desc
} else if (typeof value === 'string') {
result[locale] = value
}
}
return result
}
function getInner(value: any) {
return value?.$value ?? value?.$inner
}
function extractKeys(data: any) {
return filterKeys(data ?? {}, key => !key.startsWith('$'))
}
Schema.prototype.i18n = function i18n(messages) {
const schema = Schema(this)
const desc = mergeDesc(schema.meta.description, messages)
if (Object.keys(desc).length) schema.meta.description = desc
if (schema.dict) {
schema.dict = valueMap(schema.dict, (inner, key) => {
return inner.i18n(valueMap(messages, (data) => getInner(data)?.[key] ?? data?.[key]))
})
}
if (schema.list) {
schema.list = schema.list!.map((inner, index) => {
return inner.i18n(valueMap(messages, (data = {}) => {
if (Array.isArray(getInner(data))) return getInner(data)[index]
if (Array.isArray(data)) return data[index]
return extractKeys(data)
}))
})
}
if (schema.inner) {
schema.inner = schema.inner.i18n(valueMap(messages, (data) => {
if (getInner(data)) return getInner(data)
return extractKeys(data)
}))
}
if (schema.sKey) {
schema.sKey = schema.sKey.i18n(valueMap(messages, (data) => data?.$key))
}
return schema
}
Schema.prototype.extra = function extra(key, value) {
const schema = Schema(this)
schema.meta = { ...schema.meta, [key]: value }
return schema
}
for (const key of ['required', 'disabled', 'collapse', 'hidden', 'loose']) {
Object.assign(Schema.prototype, {
[key](this: Schema, value = true) {
const schema = Schema(this)
schema.meta = { ...schema.meta, [key]: value }
return schema
},
})
}
Schema.prototype.deprecated = function deprecated() {
const schema = Schema(this)
schema.meta.badges ||= []
schema.meta.badges.push({ text: 'deprecated', type: 'danger' })
return schema
}
Schema.prototype.experimental = function experimental() {
const schema = Schema(this)
schema.meta.badges ||= []
schema.meta.badges.push({ text: 'experimental', type: 'warning' })
return schema
}
Schema.prototype.pattern = function pattern(regexp) {
const schema = Schema(this)
const pattern = pick(regexp, ['source', 'flags'])
schema.meta = { ...schema.meta, pattern }
return schema
}
Schema.prototype.simplify = function simplify(this: Schema, value) {
if (deepEqual(value, this.meta.default, this.type === 'dict')) return null
if (isNullable(value)) return value
if (this.type === 'object' || this.type === 'dict') {
const result: Dict = {}
for (const key in value) {
const schema = this.type === 'object' ? this.dict![key] : this.inner
const item = schema?.simplify(value[key])
if (this.type === 'dict' || !isNullable(item)) result[key] = item
}
if (deepEqual(result, this.meta.default, this.type === 'dict')) return null
return result
} else if (this.type === 'array' || this.type === 'tuple') {
const result: any[] = []
;(value as any[]).forEach((value, index) => {
const schema = this.type === 'array' ? this.inner : this.list![index]
const item = schema ? schema.simplify(value) : value
result.push(item)
})
return result
} else if (this.type === 'intersect') {
const result: Dict = {}
for (const item of this.list!) {
Object.assign(result, item.simplify(value))
}
return result
} else if (this.type === 'union') {
for (const schema of this.list!) {
try {
Schema.resolve(value, schema, {})
return schema.simplify(value)
} catch {}
}
}
return value
}
Schema.prototype.toString = function toString(inline?: boolean) {
return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`
}
Schema.prototype.role = function role(role, extra) {
const schema = Schema(this)
schema.meta = { ...schema.meta, role, extra }
return schema
}
for (const key of ['default', 'link', 'comment', 'description', 'max', 'min', 'step']) {
Object.assign(Schema.prototype, {
[key](this: Schema, value: any) {
const schema = Schema(this)
schema.meta = { ...schema.meta, [key]: value }
return schema
},
})
}
const resolvers: Dict<Schemastery.Resolve> = {}
Schema.extend = function extend(type, resolve) {
resolvers[type] = resolve
}
Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
if (!schema) return [data]
if (options.ignore?.(data, schema)) return [data]
if (isNullable(data) && schema.type !== 'lazy') {
if (schema.meta.required) throw new ValidationError(`missing required value`, options)
let current = schema
let fallback = schema.meta.default
while (current?.type === 'intersect' && isNullable(fallback)) {
current = current.list![0]
fallback = current?.meta.default
}
if (isNullable(fallback)) return [data]
data = clone(fallback)
}
const callback = resolvers[schema.type]
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options)
try {
return callback(data, schema, options, strict)
} catch (error) {
if (!schema.meta.loose) throw error
return [schema.meta.default]
}
}
Schema.from = function from(source: any) {
if (isNullable(source)) {
return Schema.any()
} else if (['string', 'number', 'boolean'].includes(typeof source)) {
return Schema.const(source).required()
} else if (source[kSchema]) {
return source
} else if (typeof source === 'function') {
switch (source) {
case String: return Schema.string().required()
case Number: return Schema.number().required()
case Boolean: return Schema.boolean().required()
case Function: return Schema.function().required()
default: return Schema.is(source).required()
}
} else {
throw new TypeError(`cannot infer schema from ${source}`)
}
}
Schema.lazy = function lazy(builder) {
const toJSON = () => {
if (!schema.inner![kSchema]) {
schema.inner = schema.builder!()
schema.inner!.meta = { ...schema.meta, ...schema.inner!.meta }
}
return schema.inner!.toJSON()
}
const schema = new Schema({ type: 'lazy', builder, inner: { toJSON } as any })
return schema as any
}
Schema.natural = function natural() {
return Schema.number().step(1).min(0)
}
Schema.percent = function percent() {
return Schema.number().step(0.01).min(0).max(1).role('slider')
}
Schema.date = function date() {
return Schema.union([
Schema.is(Date),
Schema.transform(Schema.string().role('datetime'), (value, options) => {
const date = new Date(value)
if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options)
return date
}, 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'): any {
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('lazy', (data, schema, options, strict) => {
if (!schema.inner![kSchema]) {
schema.inner = schema.builder!()
schema.inner!.meta = { ...schema.meta, ...schema.inner!.meta }
}
return Schema.resolve(data, schema.inner!, options, strict)
})
Schema.extend('any', (data) => {
return [data]
})
Schema.extend('never', (data, _, options) => {
throw new ValidationError(`expected nullable but got ${data}`, options)
})
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, options: Schemastery.Options, skipMin = false) {
const { max = Infinity, min = -Infinity } = meta
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 }, 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 ValidationError(`expect string to match regexp ${regexp}`, options)
}
checkWithinRange(data.length, meta, 'string length', options)
return [data]
})
function decimalShift(data: number, digits: number) {
const str = data.toString()
if (str.includes('e')) return data * Math.pow(10, digits)
const index = str.indexOf('.')
if (index === -1) return data * Math.pow(10, digits)
const frac = str.slice(index + 1)
const integer = str.slice(0, index)
if (frac.length <= digits) return +(integer + frac.padEnd(digits, '0'))
return +(integer + frac.slice(0, digits) + '.' + frac.slice(digits))
}
function isMultipleOf(data: number, min: number, step: number) {
step = Math.abs(step)
if (!/^\d+\.\d+$/.test(step.toString())) {
return (data - min) % step === 0
}
const index = step.toString().indexOf('.')
const digits = step.toString().slice(index + 1).length
return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0
}
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 ValidationError(`expected number multiple of ${step} but got ${data}`, options)
}
return [data]
})
Schema.extend('boolean', (data, _, options) => {
if (typeof data === 'boolean') return [data]
throw new ValidationError(`expected boolean but got ${data}`, options)
})
Schema.extend('bitset', (data, { bits, meta }, options) => {
let value = 0, keys: string[] = []
if (typeof data === 'number') {
value = data
for (const key in bits!) {
if (data & bits![key]!) {
keys.push(key)
}
}
} else if (Array.isArray(data)) {
keys = data
for (const key of keys) {
if (typeof key !== 'string') throw new ValidationError(`expected string but got ${key}`, options)
if (key in bits!) value |= bits![key]!
}
} else {
throw new ValidationError(`expected number or array but got ${data}`, options)
}
if (value === meta.default) return [value]
return [value, keys]
})
Schema.extend('function', (data, _, options) => {
if (typeof data === 'function') return [data]
throw new ValidationError(`expected function but got ${data}`, options)
})
Schema.extend('is', (data, { constructor }, options) => {
if (typeof constructor === 'function') {
if (data instanceof constructor) return [data]
throw new ValidationError(`expected ${constructor.name} but got ${data}`, options)
} else {
if (isNullable(data)) {
throw new ValidationError(`expected ${constructor} but got ${data}`, options)
}
let prototype = Object.getPrototypeOf(data)
while (prototype) {
if (prototype.constructor?.name === constructor) return [data]
prototype = Object.getPrototypeOf(prototype)
}
throw new ValidationError(`expected ${constructor} but got ${data}`, options)
}
})
function property(data: any, key: keyof any, schema: Schema, options: Schemastery.Options) {
try {
const [value, adapted] = Schema.resolve(data[key], schema, {
...options,
path: [...options.path || [], key],
})
if (adapted !== undefined) data[key] = adapted
return value
} catch (e) {
if (!options?.autofix) throw e
delete data[key]
return schema.meta.default
}
}
Schema.extend('array', (data, { inner, meta }, options) => {
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))]
})
Schema.extend('dict', (data, { inner, sKey }, options, strict) => {
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options)
const result: any = {}
for (const key in data) {
let rKey: string
try {
rKey = Schema.resolve(key, sKey!, options)[0]
} catch (error) {
if (strict) continue
throw error
}
result[rKey] = property(data, key, inner!, options)
data[rKey] = data[key]
if (key !== rKey) delete data[key]
}
return [result]
})
Schema.extend('tuple', (data, { list }, options, strict) => {
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options)
const result = list!.map((inner, index) => property(data, index, inner, options))
if (strict) return [result]
result.push(...data.slice(list!.length))
return [result]
})
function merge(result: any, data: any) {
for (const key in data) {
if (key in result) continue
result[key] = data[key]
}
}
Schema.extend('object', (data, { dict }, options, strict) => {
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options)
const result: any = {}
for (const key in dict) {
const value = property(data, key, dict![key]!, options)
if (!isNullable(value) || key in data) {
result[key] = value
}
}
if (!strict) merge(result, data)
return [result]
})
Schema.extend('union', (data, { list, toString }, options, strict) => {
const messages: any[] = []
for (const inner of list!) {
try {
return Schema.resolve(data, inner, options, strict)
} catch (error) {
messages.push(error)
}
}
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options)
})
Schema.extend('intersect', (data, { list, toString }, options, strict) => {
if (!list!.length) return [data]
let result
for (const inner of list!) {
const value: any = Schema.resolve(data, inner, options, true)[0]
if (isNullable(value)) continue
if (isNullable(result)) {
result = value
} else if (typeof result !== typeof value) {
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 ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options)
}
}
if (!strict && isPlainObject(data)) merge(result, data)
return [result]
})
Schema.extend('transform', (data, { inner, callback, preserve }, options) => {
const [result, adapted = data] = Schema.resolve(data, inner!, options, true)
if (preserve) {
return [callback!(result)]
// } else if (isPlainObject(data)) {
// const temp: any = {}
// for (const key in result) {
// if (!(key in data)) continue
// temp[key] = data[key]
// delete data[key]
// }
// Object.assign(data, callback!(temp))
// return [callback!(result)]
} else {
return [callback!(result), callback!(adapted)]
}
})
type Formatter = (schema: Schema, inline?: boolean) => string
const formatters: Dict<Formatter> = {}
function defineMethod(name: string, keys: (keyof Schema)[], format: Formatter) {
formatters[name] = format
Object.assign(Schema, {
[name](...args: any[]) {
const schema = new Schema({ type: name } as Schema)
keys.forEach((key, index) => {
switch (key) {
case 'sKey': schema.sKey = args[index] ?? Schema.string(); break
case 'inner': schema.inner = Schema.from(args[index]); break
case 'list': schema.list = args[index].map(Schema.from); break
case 'dict': schema.dict = valueMap(args[index], Schema.from); break
case 'bits': {
schema.bits = {}
for (const key in args[index]) {
if (typeof args[index][key] !== 'number') continue
schema.bits[key] = args[index][key]
}
break
}
case 'callback': {
const callback = schema.callback = args[index]
;callback['toJSON'] ||= () => callback.toString()
break
}
case 'constructor': {
const constructor = schema.constructor = args[index]
if (typeof constructor === 'function') {
;constructor['toJSON'] ||= () => constructor['name']
}
break
}
default: schema[key] = args[index] as never
}
})
if (name === 'object' || name === 'dict') {
schema.meta.default = {}
} else if (name === 'array' || name === 'tuple') {
schema.meta.default = []
} else if (name === 'bitset') {
schema.meta.default = 0
}
return schema
},
})
}
defineMethod('is', ['constructor'], ({ constructor }) => {
if (typeof constructor === 'function') {
return constructor.name
} else {
return constructor!
}
})
defineMethod('any', [], () => 'any')
defineMethod('never', [], () => 'never')
defineMethod('const', ['value'], ({ value }) => typeof value === 'string' ? JSON.stringify(value) : value)
defineMethod('string', [], () => 'string')
defineMethod('number', [], () => 'number')
defineMethod('boolean', [], () => 'boolean')
defineMethod('bitset', ['bits'], () => 'bitset')
defineMethod('function', [], () => 'function')
defineMethod('array', ['inner'], ({ inner }) => `${inner!.toString(true)}[]`)
defineMethod('dict', ['inner', 'sKey'], ({ inner, sKey }) => `{ [key: ${sKey!.toString()}]: ${inner!.toString()} }`)
defineMethod('tuple', ['list'], ({ list }) => `[${list!.map((inner) => inner.toString()).join(', ')}]`)
defineMethod('object', ['dict'], ({ dict }) => {
if (Object.keys(dict!).length === 0) return '{}'
return `{ ${Object.entries(dict!).map(([key, inner]) => {
return `${key}${inner!.meta.required ? '' : '?'}: ${inner!.toString()}`
}).join(', ')} }`
})
defineMethod('union', ['list'], ({ list }, inline) => {
const result = list!.map(({ toString: format }) => format()).join(' | ')
return inline ? `(${result})` : result
})
defineMethod('intersect', ['list'], ({ list }) => {
return `${list!.map((inner) => inner.toString(true)).join(' & ')}`
})
defineMethod('transform', ['inner', 'callback', 'preserve'], ({ inner }, isInner) => inner!.toString(isInner))
export = Schema
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"module": "preserve"
},
"include": ["src"],
"references": [
{ "path": "../cosmokit" }
]
}