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

fix: avoid Errors call for nested Encode/Decode #781

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/value/transform/decode.ts
Expand Up @@ -63,7 +63,7 @@ export class TransformDecodeCheckError extends TypeBoxError {
}
}
export class TransformDecodeError extends TypeBoxError {
constructor(public readonly schema: TSchema, public readonly value: unknown, error: any) {
constructor(public readonly schema: TSchema, public readonly value: unknown, error?: any) {
super(`${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/value/transform/encode.ts
Expand Up @@ -62,7 +62,7 @@ export class TransformEncodeCheckError extends TypeBoxError {
}
}
export class TransformEncodeError extends TypeBoxError {
constructor(public readonly schema: TSchema, public readonly value: unknown, error: any) {
constructor(public readonly schema: TSchema, public readonly value: unknown, error?: any) {
super(`${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
Expand Down
56 changes: 50 additions & 6 deletions src/value/value/value.ts
Expand Up @@ -26,7 +26,7 @@ THE SOFTWARE.

---------------------------------------------------------------------------*/

import { TransformDecode, TransformEncode, TransformDecodeCheckError, TransformEncodeCheckError } from '../transform/index'
import { TransformDecode, TransformEncode, TransformDecodeCheckError, TransformEncodeCheckError, TransformEncodeError, TransformDecodeError } from '../transform/index'
import { Mutate as MutateValue, type Mutable } from '../mutate/index'
import { Hash as HashValue } from '../hash/index'
import { Equal as EqualValue } from '../equal/index'
Expand Down Expand Up @@ -87,15 +87,37 @@ export function Convert(...args: any[]) {
export function Clone<T>(value: T): T {
return CloneValue(value)
}
const decodeStack: any[][] = []
/** Decodes a value or throws if error */
export function Decode<T extends TSchema, R = StaticDecode<T>>(schema: T, references: TSchema[], value: unknown): R
/** Decodes a value or throws if error */
export function Decode<T extends TSchema, R = StaticDecode<T>>(schema: T, value: unknown): R
/** Decodes a value or throws if error */
export function Decode(...args: any[]) {
const [schema, references, value] = args.length === 3 ? [args[0], args[1], args[2]] : [args[0], [], args[1]]
if (!Check(schema, references, value)) throw new TransformDecodeCheckError(schema, value, Errors(schema, references, value).First()!)
return TransformDecode(schema, references, value)
decodeStack.push(args)
try {
if (Check(schema, references, value)) {
return TransformDecode(schema, references, value)
}
} catch(error) {
// If this is not a nested Decode call and the error is from a failed
// TransformDecode, we want to call Errors from here so the
// TransformDecodeCheckError includes the full JSON path. So the following
// check will only rethrow when we don't want to call Errors.
if (!(decodeStack.length === 1 && error instanceof TransformDecodeError)) {
throw error
}
} finally {
decodeStack.pop()
}
// If this is a nested Decode call, avoid calling Errors since it will not
// have the full JSON path. Also, it would likely produce a useless stack
// trace as most of the stack frames would be TypeBox internals.
if (decodeStack.length > 0) {
throw new TransformDecodeError(schema, value)
}
throw new TransformDecodeCheckError(schema, value, Errors(schema, references, value).First()!)
}
/** `[Mutable]` Generates missing properties on a value using default schema annotations if available. This function does not check the value and returns an unknown type. You should Check the result before use. Default is a mutable operation. To avoid mutation, Clone the value first. */
export function Default(schema: TSchema, references: TSchema[], value: unknown): unknown
Expand All @@ -105,16 +127,38 @@ export function Default(schema: TSchema, value: unknown): unknown
export function Default(...args: any[]) {
return DefaultValue.apply(DefaultValue, args as any)
}
const encodeStack: any[][] = []
/** Encodes a value or throws if error */
export function Encode<T extends TSchema, R = StaticEncode<T>>(schema: T, references: TSchema[], value: unknown): R
/** Encodes a value or throws if error */
export function Encode<T extends TSchema, R = StaticEncode<T>>(schema: T, value: unknown): R
/** Encodes a value or throws if error */
export function Encode(...args: any[]) {
const [schema, references, value] = args.length === 3 ? [args[0], args[1], args[2]] : [args[0], [], args[1]]
const encoded = TransformEncode(schema, references, value)
if (!Check(schema, references, encoded)) throw new TransformEncodeCheckError(schema, value, Errors(schema, references, value).First()!)
return encoded
encodeStack.push(args)
try {
const encoded = TransformEncode(schema, references, value)
if (Check(schema, references, encoded)) {
return encoded
}
} catch(error) {
// If this is not a nested Encode call and the error is from a failed
// TransformEncode, we want to call Errors from here so the
// TransformEncodeCheckError includes the full JSON path. So the following
// check will only rethrow when we don't want to call Errors.
if (!(encodeStack.length === 1 && error instanceof TransformEncodeError)) {
throw error
}
} finally {
encodeStack.pop()
}
// If this is a nested Encode call, avoid calling Errors since it will not
// have the full JSON path. Also, it would likely produce a useless stack
// trace as most of the stack frames would be TypeBox internals.
if (encodeStack.length > 0) {
throw new TransformEncodeError(schema, value)
}
throw new TransformEncodeCheckError(schema, value, Errors(schema, references, value).First()!)
}
/** Returns an iterator for each error in this value. */
export function Errors<T extends TSchema>(schema: T, references: TSchema[], value: unknown): ValueErrorIterator
Expand Down