Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support property path #58

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 64 additions & 21 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -33,6 +34,7 @@ declare global {
export interface Static {
<T = any>(options: Partial<Schema<T>>): Schema<T>
new <T = any>(options: Partial<Schema<T>>): Schema<T>
ValidationError: typeof ValidationError
prototype: Schema
resolve: Resolve
from<X = any>(source?: X): From<X>
Expand Down Expand Up @@ -60,6 +62,8 @@ declare global {

interface Options {
autofix?: boolean
path?: (string | number | symbol)[]
__schemastery_debug__?: boolean
}

export interface Meta<T = any> {
Expand Down Expand Up @@ -133,9 +137,35 @@ globalThis.__schemastery_index__ ??= 0

type Schema<S = any, T = S> = Schemastery<S, T>

class ValidationError extends TypeError {
[kValidationError] = true
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 Schema.ValidationError) return e;
if (e instanceof 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) {
return Schema.resolve(data, schema, options)[0]
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 Schema.ValidationError)) throw e
throw new Schema.ValidationError(e.errorMessage, e.trace)
}
} as Schema

if (options.refs) {
Expand Down Expand Up @@ -165,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
Expand Down Expand Up @@ -341,14 +373,20 @@ for (const key of ['default', 'link', 'comment', 'description', 'max', 'min', 's
const resolvers: Dict<Schemastery.Resolve> = {}

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) {
throw 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) throw errorBuilder(options)(`missing required value`)
let current = schema
let fallback = schema.meta.default
while (current?.type === 'intersect' && isNullable(fallback)) {
Expand All @@ -360,7 +398,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) throw errorBuilder(options)(`unsupported type "${schema.type}"`)

try {
return callback(data, schema, options, strict)
Expand Down Expand Up @@ -460,7 +498,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)) {
Expand Down Expand Up @@ -506,9 +544,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) {
Expand Down Expand Up @@ -603,19 +642,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) {
throw errorBuilder(options)(e)
}
})

Expand Down
Loading