From 29e40abe3c372bdadd8330dae8396353ad700c9b Mon Sep 17 00:00:00 2001 From: undefined Date: Thu, 15 Aug 2024 18:30:47 +0800 Subject: [PATCH 1/3] support property path --- packages/core/src/index.ts | 79 ++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1eabcda..d518d64 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -60,6 +60,9 @@ declare global { interface Options { autofix?: boolean + throws?: typeof ValidationError + path?: (string | number | symbol)[] + __schemastery_debug__?: boolean } export interface Meta { @@ -133,9 +136,34 @@ globalThis.__schemastery_index__ ??= 0 type Schema = Schemastery +class ValidationError extends TypeError { + constructor(public errorMessage: string, public trace: (string | number | symbol)[] = []) { + let message = ''; + for (let elem of trace || []) { + if (typeof elem === 'string') message += '.' + elem + else if (typeof elem === 'number') message += '[' + elem + ']' + else if (typeof elem === 'symbol') message += elem.description + } + if (message.startsWith('.')) message = message.slice(1) + super((message ? message + ': ' : '') + errorMessage) + } +} + +const errorBuilder = (options: Schemastery.Options = {}) => (e: any) => { + if (e instanceof (options.throws || ValidationError)) throw e; + if (typeof e === 'object' && 'message' in e) throw new (options.throws || ValidationError)(e.message, options.path) + throw e +} + const Schema = function (options: Schema) { const schema = function (data: any, options?: Schemastery.Options) { - return Schema.resolve(data, schema, options)[0] + if (options?.__schemastery_debug__) return Schema.resolve(data, schema, options)[0] + try { + return Schema.resolve(data, schema, options)[0] + } catch (e) { + if (!(e instanceof (options?.throws || ValidationError))) throw e + throw new (options?.throws || ValidationError)(e.errorMessage, e.trace) + } } as Schema if (options.refs) { @@ -341,14 +369,20 @@ for (const key of ['default', 'link', 'comment', 'description', 'max', 'min', 's const resolvers: Dict = {} Schema.extend = function extend(type, resolve) { - resolvers[type] = resolve + resolvers[type] = (data: any, schema: Schema, options: Schemastery.Options = {}, strict?: boolean) => { + try { + return resolve(data, schema, options, strict) + } catch (e) { + errorBuilder(options)(e) + } + } } Schema.resolve = function resolve(data, schema, options = {}, strict = false) { if (!schema) return [data] if (isNullable(data)) { - if (schema.meta.required) throw new TypeError(`missing required value`) + if (schema.meta.required) errorBuilder(options)(`missing required value`) let current = schema let fallback = schema.meta.default while (current?.type === 'intersect' && isNullable(fallback)) { @@ -360,7 +394,7 @@ Schema.resolve = function resolve(data, schema, options = {}, strict = false) { } const callback = resolvers[schema.type] - if (!callback) throw new TypeError(`unsupported type "${schema.type}"`) + if (!callback) errorBuilder(options)(`unsupported type "${schema.type}"`) try { return callback(data, schema, options, strict) @@ -460,7 +494,7 @@ function isMultipleOf(data: number, min: number, step: number) { } Schema.extend('number', (data, { meta }) => { - if (typeof data !== 'number') throw new TypeError(`expected number but got ${data}`) + if (typeof data !== 'number') throw new TypeError(`expected number but got ${typeof data} ${data}`) checkWithinRange(data, meta, 'number') const { step } = meta if (step && !isMultipleOf(data, meta.min ?? 0, step)) { @@ -506,9 +540,10 @@ Schema.extend('is', (data, { callback }) => { throw new TypeError(`expected ${callback!.name} but got ${data}`) }) -function property(data: any, key: keyof any, schema: Schema, options?: Schemastery.Options) { +function property(data: any, key: keyof any, schema: Schema, options: Schemastery.Options = {}) { try { - const [value, adapted] = Schema.resolve(data[key], schema, options) + const newOptions: Schemastery.Options = { ...options, path: [...(options.path || []), key] } + const [value, adapted] = Schema.resolve(data[key], schema, newOptions) if (adapted !== undefined) data[key] = adapted return value } catch (e) { @@ -603,19 +638,23 @@ Schema.extend('intersect', (data, { list, toString }, options, strict) => { 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)] + try { + 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)] + } + } catch (e) { + errorBuilder(options)(e) } }) From 193e5282cadb26e849f6fbbbab51a7d600fc4477 Mon Sep 17 00:00:00 2001 From: undefined Date: Thu, 15 Aug 2024 23:58:40 +0800 Subject: [PATCH 2/3] chore: expose `ValidationError` and remove `throws` option --- packages/core/src/index.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d518d64..e878afb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,7 @@ import { clone, deepEqual, Dict, filterKeys, isNullable, isPlainObject, pick, valueMap } from 'cosmokit' const kSchema = Symbol.for('schemastery') +const kValidationError = Symbol.for('ValidationError') declare global { namespace Schemastery { @@ -33,6 +34,7 @@ declare global { export interface Static { (options: Partial>): Schema new (options: Partial>): Schema + ValidationError: typeof ValidationError prototype: Schema resolve: Resolve from(source?: X): From @@ -60,7 +62,6 @@ declare global { interface Options { autofix?: boolean - throws?: typeof ValidationError path?: (string | number | symbol)[] __schemastery_debug__?: boolean } @@ -137,6 +138,7 @@ globalThis.__schemastery_index__ ??= 0 type Schema = Schemastery class ValidationError extends TypeError { + [kValidationError] = true constructor(public errorMessage: string, public trace: (string | number | symbol)[] = []) { let message = ''; for (let elem of trace || []) { @@ -150,19 +152,19 @@ class ValidationError extends TypeError { } const errorBuilder = (options: Schemastery.Options = {}) => (e: any) => { - if (e instanceof (options.throws || ValidationError)) throw e; - if (typeof e === 'object' && 'message' in e) throw new (options.throws || ValidationError)(e.message, options.path) - throw e + if (e instanceof Schema.ValidationError) return e; + if (typeof e === 'object' && 'message' in e) return new Schema.ValidationError(e.message, options.path) + return e } const Schema = function (options: Schema) { - const schema = function (data: any, options?: Schemastery.Options) { + const schema = function (data: any, options: Schemastery.Options = {}) { if (options?.__schemastery_debug__) return Schema.resolve(data, schema, options)[0] try { return Schema.resolve(data, schema, options)[0] } catch (e) { - if (!(e instanceof (options?.throws || ValidationError))) throw e - throw new (options?.throws || ValidationError)(e.errorMessage, e.trace) + if (!(e instanceof Schema.ValidationError)) throw e + throw new Schema.ValidationError(e.errorMessage, e.trace) } } as Schema @@ -193,6 +195,8 @@ const Schema = function (options: Schema) { return schema } as Schemastery.Static +Schema.ValidationError = ValidationError + Schema.prototype = Object.create(Function.prototype) Schema.prototype[kSchema] = true @@ -373,7 +377,7 @@ Schema.extend = function extend(type, resolve) { try { return resolve(data, schema, options, strict) } catch (e) { - errorBuilder(options)(e) + throw errorBuilder(options)(e) } } } @@ -382,7 +386,7 @@ Schema.resolve = function resolve(data, schema, options = {}, strict = false) { if (!schema) return [data] if (isNullable(data)) { - if (schema.meta.required) errorBuilder(options)(`missing required value`) + if (schema.meta.required) throw errorBuilder(options)(`missing required value`) let current = schema let fallback = schema.meta.default while (current?.type === 'intersect' && isNullable(fallback)) { @@ -394,7 +398,7 @@ Schema.resolve = function resolve(data, schema, options = {}, strict = false) { } const callback = resolvers[schema.type] - if (!callback) errorBuilder(options)(`unsupported type "${schema.type}"`) + if (!callback) throw errorBuilder(options)(`unsupported type "${schema.type}"`) try { return callback(data, schema, options, strict) @@ -654,7 +658,7 @@ Schema.extend('transform', (data, { inner, callback, preserve }, options) => { return [callback!(result), callback!(adapted)] } } catch (e) { - errorBuilder(options)(e) + throw errorBuilder(options)(e) } }) From 2b61d815dccbbf092ae1216b5304d71591643dc6 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 16 Aug 2024 16:51:13 +0800 Subject: [PATCH 3/3] Update packages/core/src/index.ts Co-authored-by: Shigma --- packages/core/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e878afb..5713011 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -153,7 +153,7 @@ class ValidationError extends TypeError { const errorBuilder = (options: Schemastery.Options = {}) => (e: any) => { if (e instanceof Schema.ValidationError) return e; - if (typeof e === 'object' && 'message' in e) return new Schema.ValidationError(e.message, options.path) + if (e instanceof Object && 'message' in e) return new Schema.ValidationError(e.message, options.path) return e }