@deepseek-ai/schemastery 3.18.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,902 @@
1
+ import { Binary, clone, deepEqual, filterKeys, isNullable, isPlainObject, pick, valueMap, type Dict } from '@deepseek-ai/cosmokit'
2
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
3
+
4
+ const kSchema = Symbol.for('schemastery')
5
+ const kValidationError = Symbol.for('ValidationError')
6
+
7
+ declare global {
8
+ namespace Schemastery {
9
+ /** Convert primitive constructors, constants, and existing schemas into a schema type. */
10
+ export type From<X> =
11
+ | X extends string | number | boolean ? Schema<X>
12
+ : X extends Schema ? X
13
+ : X extends typeof String ? Schema<string>
14
+ : X extends typeof Number ? Schema<number>
15
+ : X extends typeof Boolean ? Schema<boolean>
16
+ : X extends typeof Function ? Schema<Function, (...args: any[]) => any>
17
+ : X extends Constructor<infer S> ? Schema<S>
18
+ : never
19
+
20
+ type TypeS1<X> = X extends Schema<infer S, unknown> ? S : never
21
+ type Inverse<X> = X extends Schema<any, infer Y> ? (arg: Y) => void : never
22
+
23
+ /** Input type accepted by a schema-like value. */
24
+ export type TypeS<X> = TypeS1<From<X>>
25
+ /** Output type returned by a schema-like value after validation. */
26
+ export type TypeT<X> = ReturnType<From<X>>
27
+ /** Resolver callback used by custom schema types registered with `Schema.extend()`. */
28
+ export type Resolve = (data: any, schema: Schema, options: Options, strict?: boolean) => [any, any?]
29
+
30
+ /** Input type accepted by one schema in an intersection. */
31
+ export type IntersectS<X> = From<X> extends Schema<infer S, unknown> ? S : never
32
+ /** Output type returned by one schema in an intersection. */
33
+ export type IntersectT<X> = Inverse<From<X>> extends ((arg: infer T) => void) ? T : never
34
+
35
+ type TupleS<X extends readonly any[]> = X extends readonly [infer L, ...infer R] ? [TypeS<L>?, ...TupleS<R>] : any[]
36
+ type TupleT<X extends readonly any[]> = X extends readonly [infer L, ...infer R] ? [TypeT<L>?, ...TupleT<R>] : any[]
37
+ type ObjectS<X extends Dict> = { [K in keyof X]?: TypeS<X[K]> | null } & Dict
38
+ type ObjectT<X extends Dict> = { [K in keyof X]: TypeT<X[K]> } & Dict
39
+ type Constructor<T = any> = new (...args: any[]) => T
40
+
41
+ /** Static constructor and factory methods exposed by the default `Schema` export. */
42
+ export interface Static {
43
+ <T = any>(options: Partial<Schema<T>>): Schema<T>
44
+ new <T = any>(options: Partial<Schema<T>>): Schema<T>
45
+ prototype: Schema
46
+ /** Validate a value against a schema node and return `[output, adaptedInput?]`. */
47
+ resolve: Resolve
48
+ /** Infer a schema from a primitive value, constructor, or existing schema. */
49
+ from<X = any>(source?: X): From<X>
50
+ /** Register a resolver for a custom schema `type`. */
51
+ extend(type: string, resolve: Resolve): void
52
+ /** Accept any value without validation. */
53
+ any<T = any>(): Schema<T>
54
+ /** Accept only nullable input. */
55
+ never(): Schema<never>
56
+ /** Accept exactly one constant value. */
57
+ const<const T>(value: T): Schema<T>
58
+ /** Accept strings, with optional metadata constraints added by instance methods. */
59
+ string(): Schema<string>
60
+ /** Accept numbers, with optional range and step constraints. */
61
+ number(): Schema<number>
62
+ /** Accept non-negative integer numbers. */
63
+ natural(): Schema<number>
64
+ /** Accept a number between 0 and 1 and mark it as a slider. */
65
+ percent(): Schema<number>
66
+ /** Accept booleans. */
67
+ boolean(): Schema<boolean>
68
+ /** Accept `Date` instances or parse datetime strings into `Date` objects. */
69
+ date(): Schema<string | Date, Date>
70
+ /** Accept `RegExp` instances or parse strings into regular expressions. */
71
+ regExp(flag?: string): Schema<string | RegExp, RegExp>
72
+ /** Accept binary sources and normalize them to `ArrayBufferLike`. */
73
+ arrayBuffer(): Schema<Binary.Source, ArrayBufferLike>
74
+ arrayBuffer(encoding: 'hex' | 'base64'): Schema<Binary.Source | string, ArrayBufferLike>
75
+ /** Accept a numeric bitset or string keys and normalize to a number. */
76
+ bitset<K extends string>(bits: Partial<Record<K, number>>): Schema<number | readonly K[], number>
77
+ /** Accept functions. */
78
+ function(): Schema<Function, (...args: any[]) => any>
79
+ /** Accept instances of a constructor or objects whose constructor name matches. */
80
+ is(constructor: string): Schema
81
+ is<T>(constructor: Constructor<T>): Schema<T>
82
+ /** Accept arrays whose elements match `inner`. */
83
+ array<X>(inner: X): Schema<TypeS<X>[], TypeT<X>[]>
84
+ /** Accept plain objects with values matching `inner` and optional key schema. */
85
+ dict<X, Y extends Schema<any, string> = Schema<string>>(inner: X, sKey?: Y): Schema<Dict<TypeS<X>, TypeS<Y>>, Dict<TypeT<X>, TypeT<Y>>>
86
+ /** Accept tuple arrays where each index matches the corresponding schema. */
87
+ tuple<const X extends readonly any[]>(list: X): Schema<TupleS<X>, TupleT<X>>
88
+ /** Accept plain objects whose declared properties match the schema dictionary. */
89
+ object<X extends Dict>(dict: X): Schema<ObjectS<X>, ObjectT<X>>
90
+ /** Accept values matching at least one schema in `list`. */
91
+ union<const X>(list: readonly X[]): Schema<TypeS<X>, TypeT<X>>
92
+ /** Accept values matching every schema in `list`, merging object outputs. */
93
+ intersect<const X>(list: readonly X[]): Schema<IntersectS<X>, IntersectT<X>>
94
+ /** Validate with `inner`, then convert the result with `callback`. */
95
+ transform<X, T>(inner: X, callback: (value: TypeS<X>, options: Schemastery.Options) => T, preserve?: boolean): Schema<TypeS<X>, T>
96
+ /** Defer construction of a recursive schema until validation or serialization. */
97
+ lazy<X extends Schema>(callback: () => X): X
98
+ ValidationError: typeof ValidationError
99
+ }
100
+
101
+ /** Runtime validation options shared by all schema calls. */
102
+ interface Options {
103
+ /** Remove invalid object properties instead of throwing when possible. */
104
+ autofix?: boolean
105
+ /** Skip validation for selected values and schema nodes. */
106
+ ignore?(data: any, schema: Schema): boolean
107
+ /** Path used to format nested validation errors. */
108
+ path?: (keyof any)[]
109
+ }
110
+
111
+ /** UI and validation metadata attached by schema builder methods. */
112
+ export interface Meta<T = any> {
113
+ default?: T extends {} ? Partial<T> : T
114
+ required?: boolean
115
+ disabled?: boolean
116
+ collapse?: boolean
117
+ badges?: { text: string; type: string }[]
118
+ hidden?: boolean
119
+ loose?: boolean
120
+ role?: string
121
+ extra?: any
122
+ link?: string
123
+ description?: string | Dict<string>
124
+ comment?: string
125
+ pattern?: { source: string; flags?: string }
126
+ max?: number
127
+ min?: number
128
+ step?: number
129
+ }
130
+ }
131
+
132
+ /** Callable schema instance that validates input and returns normalized output. */
133
+ interface Schemastery<S = any, T = S> {
134
+ (data?: S | null, options?: Schemastery.Options): T
135
+ new (data?: S | null, options?: Schemastery.Options): T
136
+ [kSchema]: true
137
+ uid: number
138
+ meta: Schemastery.Meta<T>
139
+ type: string
140
+ sKey?: Schema
141
+ inner?: Schema
142
+ list?: Schema[]
143
+ dict?: Dict<Schema>
144
+ bits?: Dict<number>
145
+ callback?: Function
146
+ constructor?: string | Function
147
+ builder?: Function
148
+ value?: T
149
+ refs?: Dict<Schema>
150
+ preserve?: boolean
151
+ '~standard': StandardSchemaV1.Props // <S, T>
152
+ /** Format this schema as a compact TypeScript-like type string. */
153
+ toString(inline?: boolean): string
154
+ /** Serialize this schema, preserving shared and recursive references. */
155
+ toJSON(): Schema<S, T>
156
+ /** Mark nullable input as invalid unless a default supplies a fallback. */
157
+ required(value?: boolean): Schema<S, T>
158
+ /** Hide this schema node from UI renderers. */
159
+ hidden(value?: boolean): Schema<S, T>
160
+ /** Return the default value instead of throwing when validation fails. */
161
+ loose(value?: boolean): Schema<S, T>
162
+ /** Attach a renderer role and optional role-specific metadata. */
163
+ role(text: string, extra?: any): Schema<S, T>
164
+ /** Attach an external documentation link. */
165
+ link(link: string): Schema<S, T>
166
+ /** Set the fallback value used for nullable input. */
167
+ default(value: T): Schema<S, T>
168
+ /** Attach an auxiliary comment for documentation or form UIs. */
169
+ comment(text: string): Schema<S, T>
170
+ /** Attach a localized or plain description for documentation or form UIs. */
171
+ description(text: string): Schema<S, T>
172
+ /** Mark this schema node as disabled for form UIs. */
173
+ disabled(value?: boolean): Schema<S, T>
174
+ /** Request collapsed rendering for nested form UIs. */
175
+ collapse(value?: boolean): Schema<S, T>
176
+ /** Add a deprecated badge to this schema node. */
177
+ deprecated(): Schema<S, T>
178
+ /** Add an experimental badge to this schema node. */
179
+ experimental(): Schema<S, T>
180
+ /** Require strings to match a regular expression. */
181
+ pattern(regexp: RegExp): Schema<S, T>
182
+ /** Set an inclusive maximum for numbers or collection lengths. */
183
+ max(value: number): Schema<S, T>
184
+ /** Set an inclusive minimum for numbers or collection lengths. */
185
+ min(value: number): Schema<S, T>
186
+ /** Set the numeric increment constraint. */
187
+ step(value: number): Schema<S, T>
188
+ /** Add or replace an object property schema. */
189
+ set(key: string, value: Schema): Schema<S, T>
190
+ /** Append a tuple, union, or intersection member schema. */
191
+ push(value: Schema): Schema<S, T>
192
+ /** Remove values equal to schema defaults from normalized output. */
193
+ simplify(value?: any): any
194
+ /** Return a schema clone with descriptions merged from locale messages. */
195
+ i18n(messages: Dict): Schema<S, T>
196
+ /** Attach arbitrary metadata consumed by form renderers and downstream tools. */
197
+ extra<K extends keyof Schemastery.Meta>(key: K, value: Schemastery.Meta[K]): Schema<S, T>
198
+ }
199
+ }
200
+
201
+ declare namespace globalThis {
202
+ // eslint-disable-next-line @typescript-eslint/naming-convention
203
+ export let __schemastery_index__: number
204
+ export let __schemastery_refs__: Record<number, Schema> | undefined
205
+ }
206
+
207
+ globalThis.__schemastery_index__ ??= 0
208
+ globalThis.__schemastery_refs__ = undefined
209
+
210
+ class ValidationError extends TypeError {
211
+ name = 'ValidationError'
212
+
213
+ constructor(message: string, public options: Schemastery.Options) {
214
+ let prefix = '$'
215
+ for (const segment of options.path || []) {
216
+ if (typeof segment === 'string') {
217
+ prefix += '.' + segment
218
+ } else if (typeof segment === 'number') {
219
+ prefix += '[' + segment + ']'
220
+ } else if (typeof segment === 'symbol') {
221
+ prefix += `[Symbol(${segment.toString()})]`
222
+ }
223
+ }
224
+ if (prefix.startsWith('.')) prefix = prefix.slice(1)
225
+ super((prefix === '$' ? '' : `${prefix} `) + message)
226
+ }
227
+
228
+ static is(error: any): error is ValidationError {
229
+ return !!error?.[kValidationError]
230
+ }
231
+ }
232
+
233
+ Object.defineProperty(ValidationError.prototype, kValidationError, {
234
+ value: true,
235
+ })
236
+
237
+ type Schema<S = any, T = S> = Schemastery<S, T>
238
+
239
+ const Schema = function (options: Schema) {
240
+ const schema = function (data: any, options: Schemastery.Options = {}) {
241
+ return Schema.resolve(data, schema, options)[0]
242
+ } as Schema
243
+
244
+ if (options.refs) {
245
+ const refs = valueMap(options.refs, options => new Schema(options))
246
+ const getRef = (uid: any) => refs[uid]!
247
+ for (const key in refs) {
248
+ const options = refs[key]!
249
+ options.sKey = getRef(options.sKey)
250
+ options.inner = getRef(options.inner)
251
+ options.list = options.list && options.list.map(getRef)
252
+ options.dict = options.dict && valueMap(options.dict, getRef)
253
+ }
254
+ return refs[options.uid!]
255
+ }
256
+
257
+ Object.assign(schema, options)
258
+ if (typeof schema.callback === 'string') {
259
+ try {
260
+ // eslint-disable-next-line no-new-func
261
+ schema.callback = new Function('return ' + schema.callback)()
262
+ } catch {}
263
+ }
264
+ Object.defineProperty(schema, 'uid', { value: globalThis.__schemastery_index__++ })
265
+ Object.setPrototypeOf(schema, Schema.prototype)
266
+ schema.meta ||= {}
267
+ schema.toString = schema.toString.bind(schema)
268
+ return schema
269
+ } as Schemastery.Static
270
+
271
+ Schema.prototype = Object.create(Function.prototype)
272
+
273
+ Schema.prototype[kSchema] = true
274
+
275
+ Object.defineProperty(Schema.prototype, '~standard', {
276
+ get(this: Schema) {
277
+ return {
278
+ version: 1,
279
+ vendor: 'schemastery',
280
+ validate: (value: unknown) => {
281
+ try {
282
+ return { value: Schema.resolve(value, this, {})[0] }
283
+ } catch (error) {
284
+ if (ValidationError.is(error)) {
285
+ return { issues: [{ message: error.message, path: error.options.path }] }
286
+ }
287
+ throw error
288
+ }
289
+ },
290
+ }
291
+ },
292
+ })
293
+
294
+ Schema.ValidationError = ValidationError
295
+
296
+ Schema.prototype.toJSON = function toJSON() {
297
+ if (globalThis.__schemastery_refs__) {
298
+ globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }))
299
+ return this.uid as any
300
+ }
301
+
302
+ globalThis.__schemastery_refs__ = { [this.uid]: { ...this } as Schema }
303
+ globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }))
304
+ const result = { uid: this.uid, refs: globalThis.__schemastery_refs__ }
305
+ globalThis.__schemastery_refs__ = undefined
306
+ return result
307
+ }
308
+
309
+ Schema.prototype.set = function set(key, value) {
310
+ this.dict![key] = value
311
+ return this
312
+ }
313
+
314
+ Schema.prototype.push = function push(value) {
315
+ this.list!.push(value)
316
+ return this
317
+ }
318
+
319
+ function mergeDesc(original: undefined | string | Dict<string>, messages: Dict) {
320
+ const result: Dict<string> = typeof original === 'string' ? { '': original } : { ...original }
321
+ for (const locale in messages) {
322
+ const value = messages[locale]
323
+ if (value?.$description || value?.$desc) {
324
+ result[locale] = value.$description || value.$desc
325
+ } else if (typeof value === 'string') {
326
+ result[locale] = value
327
+ }
328
+ }
329
+ return result
330
+ }
331
+
332
+ function getInner(value: any) {
333
+ return value?.$value ?? value?.$inner
334
+ }
335
+
336
+ function extractKeys(data: any) {
337
+ return filterKeys(data ?? {}, key => !key.startsWith('$'))
338
+ }
339
+
340
+ Schema.prototype.i18n = function i18n(messages) {
341
+ const schema = Schema(this)
342
+ const desc = mergeDesc(schema.meta.description, messages)
343
+ if (Object.keys(desc).length) schema.meta.description = desc
344
+ if (schema.dict) {
345
+ schema.dict = valueMap(schema.dict, (inner, key) => {
346
+ return inner.i18n(valueMap(messages, (data) => getInner(data)?.[key] ?? data?.[key]))
347
+ })
348
+ }
349
+ if (schema.list) {
350
+ schema.list = schema.list!.map((inner, index) => {
351
+ return inner.i18n(valueMap(messages, (data = {}) => {
352
+ if (Array.isArray(getInner(data))) return getInner(data)[index]
353
+ if (Array.isArray(data)) return data[index]
354
+ return extractKeys(data)
355
+ }))
356
+ })
357
+ }
358
+ if (schema.inner) {
359
+ schema.inner = schema.inner.i18n(valueMap(messages, (data) => {
360
+ if (getInner(data)) return getInner(data)
361
+ return extractKeys(data)
362
+ }))
363
+ }
364
+ if (schema.sKey) {
365
+ schema.sKey = schema.sKey.i18n(valueMap(messages, (data) => data?.$key))
366
+ }
367
+ return schema
368
+ }
369
+
370
+ Schema.prototype.extra = function extra(key, value) {
371
+ const schema = Schema(this)
372
+ schema.meta = { ...schema.meta, [key]: value }
373
+ return schema
374
+ }
375
+
376
+ for (const key of ['required', 'disabled', 'collapse', 'hidden', 'loose']) {
377
+ Object.assign(Schema.prototype, {
378
+ [key](this: Schema, value = true) {
379
+ const schema = Schema(this)
380
+ schema.meta = { ...schema.meta, [key]: value }
381
+ return schema
382
+ },
383
+ })
384
+ }
385
+
386
+ Schema.prototype.deprecated = function deprecated() {
387
+ const schema = Schema(this)
388
+ schema.meta.badges ||= []
389
+ schema.meta.badges.push({ text: 'deprecated', type: 'danger' })
390
+ return schema
391
+ }
392
+
393
+ Schema.prototype.experimental = function experimental() {
394
+ const schema = Schema(this)
395
+ schema.meta.badges ||= []
396
+ schema.meta.badges.push({ text: 'experimental', type: 'warning' })
397
+ return schema
398
+ }
399
+
400
+ Schema.prototype.pattern = function pattern(regexp) {
401
+ const schema = Schema(this)
402
+ const pattern = pick(regexp, ['source', 'flags'])
403
+ schema.meta = { ...schema.meta, pattern }
404
+ return schema
405
+ }
406
+
407
+ Schema.prototype.simplify = function simplify(this: Schema, value) {
408
+ if (deepEqual(value, this.meta.default, this.type === 'dict')) return null
409
+ if (isNullable(value)) return value
410
+ if (this.type === 'object' || this.type === 'dict') {
411
+ const result: Dict = {}
412
+ for (const key in value) {
413
+ const schema = this.type === 'object' ? this.dict![key] : this.inner
414
+ const item = schema?.simplify(value[key])
415
+ if (this.type === 'dict' || !isNullable(item)) result[key] = item
416
+ }
417
+ if (deepEqual(result, this.meta.default, this.type === 'dict')) return null
418
+ return result
419
+ } else if (this.type === 'array' || this.type === 'tuple') {
420
+ const result: any[] = []
421
+ ;(value as any[]).forEach((value, index) => {
422
+ const schema = this.type === 'array' ? this.inner : this.list![index]
423
+ const item = schema ? schema.simplify(value) : value
424
+ result.push(item)
425
+ })
426
+ return result
427
+ } else if (this.type === 'intersect') {
428
+ const result: Dict = {}
429
+ for (const item of this.list!) {
430
+ Object.assign(result, item.simplify(value))
431
+ }
432
+ return result
433
+ } else if (this.type === 'union') {
434
+ for (const schema of this.list!) {
435
+ try {
436
+ Schema.resolve(value, schema, {})
437
+ return schema.simplify(value)
438
+ } catch {}
439
+ }
440
+ }
441
+ return value
442
+ }
443
+
444
+ Schema.prototype.toString = function toString(inline?: boolean) {
445
+ return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`
446
+ }
447
+
448
+ Schema.prototype.role = function role(role, extra) {
449
+ const schema = Schema(this)
450
+ schema.meta = { ...schema.meta, role, extra }
451
+ return schema
452
+ }
453
+
454
+ for (const key of ['default', 'link', 'comment', 'description', 'max', 'min', 'step']) {
455
+ Object.assign(Schema.prototype, {
456
+ [key](this: Schema, value: any) {
457
+ const schema = Schema(this)
458
+ schema.meta = { ...schema.meta, [key]: value }
459
+ return schema
460
+ },
461
+ })
462
+ }
463
+
464
+ const resolvers: Dict<Schemastery.Resolve> = {}
465
+
466
+ Schema.extend = function extend(type, resolve) {
467
+ resolvers[type] = resolve
468
+ }
469
+
470
+ Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
471
+ if (!schema) return [data]
472
+ if (options.ignore?.(data, schema)) return [data]
473
+
474
+ if (isNullable(data) && schema.type !== 'lazy') {
475
+ if (schema.meta.required) throw new ValidationError(`missing required value`, options)
476
+ let current = schema
477
+ let fallback = schema.meta.default
478
+ while (current?.type === 'intersect' && isNullable(fallback)) {
479
+ current = current.list![0]
480
+ fallback = current?.meta.default
481
+ }
482
+ if (isNullable(fallback)) return [data]
483
+ data = clone(fallback)
484
+ }
485
+
486
+ const callback = resolvers[schema.type]
487
+ if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options)
488
+
489
+ try {
490
+ return callback(data, schema, options, strict)
491
+ } catch (error) {
492
+ if (!schema.meta.loose) throw error
493
+ return [schema.meta.default]
494
+ }
495
+ }
496
+
497
+ Schema.from = function from(source: any) {
498
+ if (isNullable(source)) {
499
+ return Schema.any()
500
+ } else if (['string', 'number', 'boolean'].includes(typeof source)) {
501
+ return Schema.const(source).required()
502
+ } else if (source[kSchema]) {
503
+ return source
504
+ } else if (typeof source === 'function') {
505
+ switch (source) {
506
+ case String: return Schema.string().required()
507
+ case Number: return Schema.number().required()
508
+ case Boolean: return Schema.boolean().required()
509
+ case Function: return Schema.function().required()
510
+ default: return Schema.is(source).required()
511
+ }
512
+ } else {
513
+ throw new TypeError(`cannot infer schema from ${source}`)
514
+ }
515
+ }
516
+
517
+ Schema.lazy = function lazy(builder) {
518
+ const toJSON = () => {
519
+ if (!schema.inner![kSchema]) {
520
+ schema.inner = schema.builder!()
521
+ schema.inner!.meta = { ...schema.meta, ...schema.inner!.meta }
522
+ }
523
+ return schema.inner!.toJSON()
524
+ }
525
+ const schema = new Schema({ type: 'lazy', builder, inner: { toJSON } as any })
526
+ return schema as any
527
+ }
528
+
529
+ Schema.natural = function natural() {
530
+ return Schema.number().step(1).min(0)
531
+ }
532
+
533
+ Schema.percent = function percent() {
534
+ return Schema.number().step(0.01).min(0).max(1).role('slider')
535
+ }
536
+
537
+ Schema.date = function date() {
538
+ return Schema.union([
539
+ Schema.is(Date),
540
+ Schema.transform(Schema.string().role('datetime'), (value, options) => {
541
+ const date = new Date(value)
542
+ if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options)
543
+ return date
544
+ }, true),
545
+ ])
546
+ }
547
+
548
+ Schema.regExp = function regExp(flag = '') {
549
+ return Schema.union([
550
+ Schema.is(RegExp),
551
+ Schema.transform(Schema.string().role('regexp', { flag }), (value, options) => {
552
+ try {
553
+ return new RegExp(value, flag)
554
+ } catch (e: any) {
555
+ throw new ValidationError(e.message, options)
556
+ }
557
+ }, true),
558
+ ])
559
+ }
560
+
561
+ Schema.arrayBuffer = function arrayBuffer(encoding?: 'hex' | 'base64'): any {
562
+ return Schema.union([
563
+ Schema.is(ArrayBuffer),
564
+ Schema.is(SharedArrayBuffer),
565
+ Schema.transform(Schema.any<ArrayBufferView>(), (value, options) => {
566
+ if (Binary.isSource(value)) return Binary.fromSource(value)
567
+ throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options)
568
+ }, true),
569
+ ...encoding ? [Schema.transform(Schema.string(), (value, options) => {
570
+ try {
571
+ return encoding === 'base64'
572
+ ? Binary.fromBase64(value)
573
+ : Binary.fromHex(value)
574
+ } catch (e: any) {
575
+ throw new ValidationError(e.message, options)
576
+ }
577
+ }, true)] as const : [],
578
+ ])
579
+ }
580
+
581
+ Schema.extend('lazy', (data, schema, options, strict) => {
582
+ if (!schema.inner![kSchema]) {
583
+ schema.inner = schema.builder!()
584
+ schema.inner!.meta = { ...schema.meta, ...schema.inner!.meta }
585
+ }
586
+ return Schema.resolve(data, schema.inner!, options, strict)
587
+ })
588
+
589
+ Schema.extend('any', (data) => {
590
+ return [data]
591
+ })
592
+
593
+ Schema.extend('never', (data, _, options) => {
594
+ throw new ValidationError(`expected nullable but got ${data}`, options)
595
+ })
596
+
597
+ Schema.extend('const', (data, { value }, options) => {
598
+ if (deepEqual(data, value)) return [value]
599
+ throw new ValidationError(`expected ${value} but got ${data}`, options)
600
+ })
601
+
602
+ function checkWithinRange(data: number, meta: Schemastery.Meta<any>, description: string, options: Schemastery.Options, skipMin = false) {
603
+ const { max = Infinity, min = -Infinity } = meta
604
+ if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options)
605
+ if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options)
606
+ }
607
+
608
+ Schema.extend('string', (data, { meta }, options) => {
609
+ if (typeof data !== 'string') throw new ValidationError(`expected string but got ${data}`, options)
610
+ if (meta.pattern) {
611
+ const regexp = new RegExp(meta.pattern.source, meta.pattern.flags)
612
+ if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options)
613
+ }
614
+ checkWithinRange(data.length, meta, 'string length', options)
615
+ return [data]
616
+ })
617
+
618
+ function decimalShift(data: number, digits: number) {
619
+ const str = data.toString()
620
+ if (str.includes('e')) return data * Math.pow(10, digits)
621
+ const index = str.indexOf('.')
622
+ if (index === -1) return data * Math.pow(10, digits)
623
+ const frac = str.slice(index + 1)
624
+ const integer = str.slice(0, index)
625
+ if (frac.length <= digits) return +(integer + frac.padEnd(digits, '0'))
626
+ return +(integer + frac.slice(0, digits) + '.' + frac.slice(digits))
627
+ }
628
+
629
+ function isMultipleOf(data: number, min: number, step: number) {
630
+ step = Math.abs(step)
631
+ if (!/^\d+\.\d+$/.test(step.toString())) {
632
+ return (data - min) % step === 0
633
+ }
634
+ const index = step.toString().indexOf('.')
635
+ const digits = step.toString().slice(index + 1).length
636
+ return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0
637
+ }
638
+
639
+ Schema.extend('number', (data, { meta }, options) => {
640
+ if (typeof data !== 'number') throw new ValidationError(`expected number but got ${data}`, options)
641
+ checkWithinRange(data, meta, 'number', options)
642
+ const { step } = meta
643
+ if (step && !isMultipleOf(data, meta.min ?? 0, step)) {
644
+ throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options)
645
+ }
646
+ return [data]
647
+ })
648
+
649
+ Schema.extend('boolean', (data, _, options) => {
650
+ if (typeof data === 'boolean') return [data]
651
+ throw new ValidationError(`expected boolean but got ${data}`, options)
652
+ })
653
+
654
+ Schema.extend('bitset', (data, { bits, meta }, options) => {
655
+ let value = 0, keys: string[] = []
656
+ if (typeof data === 'number') {
657
+ value = data
658
+ for (const key in bits!) {
659
+ if (data & bits![key]!) {
660
+ keys.push(key)
661
+ }
662
+ }
663
+ } else if (Array.isArray(data)) {
664
+ keys = data
665
+ for (const key of keys) {
666
+ if (typeof key !== 'string') throw new ValidationError(`expected string but got ${key}`, options)
667
+ if (key in bits!) value |= bits![key]!
668
+ }
669
+ } else {
670
+ throw new ValidationError(`expected number or array but got ${data}`, options)
671
+ }
672
+ if (value === meta.default) return [value]
673
+ return [value, keys]
674
+ })
675
+
676
+ Schema.extend('function', (data, _, options) => {
677
+ if (typeof data === 'function') return [data]
678
+ throw new ValidationError(`expected function but got ${data}`, options)
679
+ })
680
+
681
+ Schema.extend('is', (data, { constructor }, options) => {
682
+ if (typeof constructor === 'function') {
683
+ if (data instanceof constructor) return [data]
684
+ throw new ValidationError(`expected ${constructor.name} but got ${data}`, options)
685
+ } else {
686
+ if (isNullable(data)) {
687
+ throw new ValidationError(`expected ${constructor} but got ${data}`, options)
688
+ }
689
+ let prototype = Object.getPrototypeOf(data)
690
+ while (prototype) {
691
+ if (prototype.constructor?.name === constructor) return [data]
692
+ prototype = Object.getPrototypeOf(prototype)
693
+ }
694
+ throw new ValidationError(`expected ${constructor} but got ${data}`, options)
695
+ }
696
+ })
697
+
698
+ function property(data: any, key: keyof any, schema: Schema, options: Schemastery.Options) {
699
+ try {
700
+ const [value, adapted] = Schema.resolve(data[key], schema, {
701
+ ...options,
702
+ path: [...options.path || [], key],
703
+ })
704
+ if (adapted !== undefined) data[key] = adapted
705
+ return value
706
+ } catch (e) {
707
+ if (!options?.autofix) throw e
708
+ delete data[key]
709
+ return schema.meta.default
710
+ }
711
+ }
712
+
713
+ Schema.extend('array', (data, { inner, meta }, options) => {
714
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options)
715
+ checkWithinRange(data.length, meta, 'array length', options, !isNullable(inner!.meta.default))
716
+ return [data.map((_, index) => property(data, index, inner!, options))]
717
+ })
718
+
719
+ Schema.extend('dict', (data, { inner, sKey }, options, strict) => {
720
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options)
721
+ const result: any = {}
722
+ for (const key in data) {
723
+ let rKey: string
724
+ try {
725
+ rKey = Schema.resolve(key, sKey!, options)[0]
726
+ } catch (error) {
727
+ if (strict) continue
728
+ throw error
729
+ }
730
+ result[rKey] = property(data, key, inner!, options)
731
+ data[rKey] = data[key]
732
+ if (key !== rKey) delete data[key]
733
+ }
734
+ return [result]
735
+ })
736
+
737
+ Schema.extend('tuple', (data, { list }, options, strict) => {
738
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options)
739
+ const result = list!.map((inner, index) => property(data, index, inner, options))
740
+ if (strict) return [result]
741
+ result.push(...data.slice(list!.length))
742
+ return [result]
743
+ })
744
+
745
+ function merge(result: any, data: any) {
746
+ for (const key in data) {
747
+ if (key in result) continue
748
+ result[key] = data[key]
749
+ }
750
+ }
751
+
752
+ Schema.extend('object', (data, { dict }, options, strict) => {
753
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options)
754
+ const result: any = {}
755
+ for (const key in dict) {
756
+ const value = property(data, key, dict![key]!, options)
757
+ if (!isNullable(value) || key in data) {
758
+ result[key] = value
759
+ }
760
+ }
761
+ if (!strict) merge(result, data)
762
+ return [result]
763
+ })
764
+
765
+ Schema.extend('union', (data, { list, toString }, options, strict) => {
766
+ const messages: any[] = []
767
+ for (const inner of list!) {
768
+ try {
769
+ return Schema.resolve(data, inner, options, strict)
770
+ } catch (error) {
771
+ messages.push(error)
772
+ }
773
+ }
774
+ throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options)
775
+ })
776
+
777
+ Schema.extend('intersect', (data, { list, toString }, options, strict) => {
778
+ if (!list!.length) return [data]
779
+ let result
780
+ for (const inner of list!) {
781
+ const value: any = Schema.resolve(data, inner, options, true)[0]
782
+ if (isNullable(value)) continue
783
+ if (isNullable(result)) {
784
+ result = value
785
+ } else if (typeof result !== typeof value) {
786
+ throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options)
787
+ } else if (typeof value === 'object') {
788
+ merge(result ??= {}, value)
789
+ } else if (result !== value) {
790
+ throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options)
791
+ }
792
+ }
793
+ if (!strict && isPlainObject(data)) merge(result, data)
794
+ return [result]
795
+ })
796
+
797
+ Schema.extend('transform', (data, { inner, callback, preserve }, options) => {
798
+ const [result, adapted = data] = Schema.resolve(data, inner!, options, true)
799
+ if (preserve) {
800
+ return [callback!(result)]
801
+ // } else if (isPlainObject(data)) {
802
+ // const temp: any = {}
803
+ // for (const key in result) {
804
+ // if (!(key in data)) continue
805
+ // temp[key] = data[key]
806
+ // delete data[key]
807
+ // }
808
+ // Object.assign(data, callback!(temp))
809
+ // return [callback!(result)]
810
+ } else {
811
+ return [callback!(result), callback!(adapted)]
812
+ }
813
+ })
814
+
815
+ type Formatter = (schema: Schema, inline?: boolean) => string
816
+ const formatters: Dict<Formatter> = {}
817
+
818
+ function defineMethod(name: string, keys: (keyof Schema)[], format: Formatter) {
819
+ formatters[name] = format
820
+ Object.assign(Schema, {
821
+ [name](...args: any[]) {
822
+ const schema = new Schema({ type: name } as Schema)
823
+ keys.forEach((key, index) => {
824
+ switch (key) {
825
+ case 'sKey': schema.sKey = args[index] ?? Schema.string(); break
826
+ case 'inner': schema.inner = Schema.from(args[index]); break
827
+ case 'list': schema.list = args[index].map(Schema.from); break
828
+ case 'dict': schema.dict = valueMap(args[index], Schema.from); break
829
+ case 'bits': {
830
+ schema.bits = {}
831
+ for (const key in args[index]) {
832
+ if (typeof args[index][key] !== 'number') continue
833
+ schema.bits[key] = args[index][key]
834
+ }
835
+ break
836
+ }
837
+ case 'callback': {
838
+ const callback = schema.callback = args[index]
839
+ ;callback['toJSON'] ||= () => callback.toString()
840
+ break
841
+ }
842
+ case 'constructor': {
843
+ const constructor = schema.constructor = args[index]
844
+ if (typeof constructor === 'function') {
845
+ ;constructor['toJSON'] ||= () => constructor['name']
846
+ }
847
+ break
848
+ }
849
+ default: schema[key] = args[index] as never
850
+ }
851
+ })
852
+ if (name === 'object' || name === 'dict') {
853
+ schema.meta.default = {}
854
+ } else if (name === 'array' || name === 'tuple') {
855
+ schema.meta.default = []
856
+ } else if (name === 'bitset') {
857
+ schema.meta.default = 0
858
+ }
859
+ return schema
860
+ },
861
+ })
862
+ }
863
+
864
+ defineMethod('is', ['constructor'], ({ constructor }) => {
865
+ if (typeof constructor === 'function') {
866
+ return constructor.name
867
+ } else {
868
+ return constructor!
869
+ }
870
+ })
871
+
872
+ defineMethod('any', [], () => 'any')
873
+ defineMethod('never', [], () => 'never')
874
+ defineMethod('const', ['value'], ({ value }) => typeof value === 'string' ? JSON.stringify(value) : value)
875
+ defineMethod('string', [], () => 'string')
876
+ defineMethod('number', [], () => 'number')
877
+ defineMethod('boolean', [], () => 'boolean')
878
+ defineMethod('bitset', ['bits'], () => 'bitset')
879
+ defineMethod('function', [], () => 'function')
880
+ defineMethod('array', ['inner'], ({ inner }) => `${inner!.toString(true)}[]`)
881
+ defineMethod('dict', ['inner', 'sKey'], ({ inner, sKey }) => `{ [key: ${sKey!.toString()}]: ${inner!.toString()} }`)
882
+ defineMethod('tuple', ['list'], ({ list }) => `[${list!.map((inner) => inner.toString()).join(', ')}]`)
883
+
884
+ defineMethod('object', ['dict'], ({ dict }) => {
885
+ if (Object.keys(dict!).length === 0) return '{}'
886
+ return `{ ${Object.entries(dict!).map(([key, inner]) => {
887
+ return `${key}${inner!.meta.required ? '' : '?'}: ${inner!.toString()}`
888
+ }).join(', ')} }`
889
+ })
890
+
891
+ defineMethod('union', ['list'], ({ list }, inline) => {
892
+ const result = list!.map(({ toString: format }) => format()).join(' | ')
893
+ return inline ? `(${result})` : result
894
+ })
895
+
896
+ defineMethod('intersect', ['list'], ({ list }) => {
897
+ return `${list!.map((inner) => inner.toString(true)).join(' & ')}`
898
+ })
899
+
900
+ defineMethod('transform', ['inner', 'callback', 'preserve'], ({ inner }, isInner) => inner!.toString(isInner))
901
+
902
+ export default Schema