index.ts 32 KB

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