diff --git a/package-lock.json b/package-lock.json index c70b6b3..3804e54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.32.15", + "version": "0.32.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.32.15", + "version": "0.32.16", "license": "MIT", "devDependencies": { "@arethetypeswrong/cli": "^0.13.2", diff --git a/package.json b/package.json index 6fa87a1..e4c6fe9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.32.15", + "version": "0.32.16", "description": "Json Schema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/src/type/indexed/indexed.ts b/src/type/indexed/indexed.ts index 2589aea..f3df37e 100644 --- a/src/type/indexed/indexed.ts +++ b/src/type/indexed/indexed.ts @@ -88,12 +88,41 @@ function FromIntersect(T: [...T], K: } // ------------------------------------------------------------------ // FromUnionRest +// +// The following accept a tuple of indexed key results. When evaluating +// these results, we check if any result evaluated to TNever. For key +// indexed unions, a TNever result indicates that the key was not +// present on the variant. In these cases, we must evaluate the indexed +// union to TNever (as given by a [] result). This logic aligns to the +// following behaviour. +// +// Non-Overlapping Union +// +// type A = { a: string } +// type B = { b: string } +// type C = (A | B) & { a: number } // C is { a: number } +// +// Overlapping Union +// +// type A = { a: string } +// type B = { a: string } +// type C = (A | B) & { a: number } // C is { a: never } +// // ------------------------------------------------------------------ // prettier-ignore -type TFromUnionRest = T +type TFromUnionRest = + T extends [infer L extends TSchema, ...infer R extends TSchema[]] + ? L extends TNever + ? [] + : TFromUnionRest + : Acc // prettier-ignore function FromUnionRest(T: [...T]): TFromUnionRest { - return T as never // review this + return ( + T.some(L => IsNever(L)) + ? [] + : T + ) as never } // ------------------------------------------------------------------ // FromUnion diff --git a/src/type/template-literal/parse.ts b/src/type/template-literal/parse.ts index 7a2730f..6a36c01 100644 --- a/src/type/template-literal/parse.ts +++ b/src/type/template-literal/parse.ts @@ -40,23 +40,42 @@ export type Expression = ExpressionAnd | ExpressionOr | ExpressionConst export type ExpressionConst = { type: 'const'; const: string } export type ExpressionAnd = { type: 'and'; expr: Expression[] } export type ExpressionOr = { type: 'or'; expr: Expression[] } +// ------------------------------------------------------------------- +// Unescape +// +// Unescape for these control characters specifically. Note that this +// function is only called on non union group content, and where we +// still want to allow the user to embed control characters in that +// content. For review. +// ------------------------------------------------------------------- // prettier-ignore +function Unescape(pattern: string) { + return pattern + .replace(/\\\$/g, '$') + .replace(/\\\*/g, '*') + .replace(/\\\^/g, '^') + .replace(/\\\|/g, '|') + .replace(/\\\(/g, '(') + .replace(/\\\)/g, ')') +} +// ------------------------------------------------------------------- +// Control Characters +// ------------------------------------------------------------------- function IsNonEscaped(pattern: string, index: number, char: string) { return pattern[index] === char && pattern.charCodeAt(index - 1) !== 92 } -// prettier-ignore function IsOpenParen(pattern: string, index: number) { return IsNonEscaped(pattern, index, '(') } -// prettier-ignore function IsCloseParen(pattern: string, index: number) { return IsNonEscaped(pattern, index, ')') } -// prettier-ignore function IsSeparator(pattern: string, index: number) { return IsNonEscaped(pattern, index, '|') } -// prettier-ignore +// ------------------------------------------------------------------- +// Control Groups +// ------------------------------------------------------------------- function IsGroup(pattern: string) { if (!(IsOpenParen(pattern, 0) && IsCloseParen(pattern, pattern.length - 1))) return false let count = 0 @@ -155,7 +174,7 @@ export function TemplateLiteralParse(pattern: string): Expression { IsGroup(pattern) ? TemplateLiteralParse(InGroup(pattern)) : IsPrecedenceOr(pattern) ? Or(pattern) : IsPrecedenceAnd(pattern) ? And(pattern) : - { type: 'const', const: pattern } + { type: 'const', const: Unescape(pattern) } ) } // ------------------------------------------------------------------ diff --git a/src/type/template-literal/syntax.ts b/src/type/template-literal/syntax.ts index 1250ee6..3352b10 100644 --- a/src/type/template-literal/syntax.ts +++ b/src/type/template-literal/syntax.ts @@ -100,7 +100,10 @@ type FromTerminal = // prettier-ignore type FromString = T extends `{${infer L}}${infer R}` ? [FromTerminal, ...FromString] : - T extends `${infer L}$${infer R}` ? [TLiteral, ...FromString] : + // note: to correctly handle $ characters encoded in the sequence, we need to + // lookahead and test against opening and closing union groups. + T extends `${infer L}$\{${infer R1}\}${infer R2}` ? [TLiteral, ...FromString<`{${R1}}`>, ...FromString] : + T extends `${infer L}$\{${infer R1}\}` ? [TLiteral, ...FromString<`{${R1}}`>] : T extends `${infer L}` ? [TLiteral] : [] diff --git a/src/value/convert/convert.ts b/src/value/convert/convert.ts index 35afea3..dd0e2d1 100644 --- a/src/value/convert/convert.ts +++ b/src/value/convert/convert.ts @@ -183,11 +183,8 @@ function FromDate(schema: TDate, references: TSchema[], value: any): unknown { function FromInteger(schema: TInteger, references: TSchema[], value: any): unknown { return TryConvertInteger(value) } -// prettier-ignore function FromIntersect(schema: TIntersect, references: TSchema[], value: any): unknown { - const allObjects = schema.allOf.every(schema => IsObjectType(schema)) - if(allObjects) return Visit(Composite(schema.allOf as TObject[]), references, value) - return Visit(schema.allOf[0], references, value) // todo: fix this + return schema.allOf.reduce((value, schema) => Visit(schema, references, value), value) } function FromLiteral(schema: TLiteral, references: TSchema[], value: any): unknown { return TryConvertLiteral(schema, value) @@ -243,13 +240,7 @@ function FromUndefined(schema: TUndefined, references: TSchema[], value: any): u return TryConvertUndefined(value) } function FromUnion(schema: TUnion, references: TSchema[], value: any): unknown { - for (const subschema of schema.anyOf) { - const converted = Visit(subschema, references, value) - if (Check(subschema, references, converted)) { - return converted - } - } - return value + return schema.anyOf.reduce((value, schema) => Visit(schema, references, value), value) } function Visit(schema: TSchema, references: TSchema[], value: any): unknown { const references_ = IsString(schema.$id) ? [...references, schema] : references diff --git a/src/value/transform/decode.ts b/src/value/transform/decode.ts index 3169293..02982d0 100644 --- a/src/value/transform/decode.ts +++ b/src/value/transform/decode.ts @@ -57,153 +57,164 @@ import { IsTransform, IsSchema } from '../../type/guard/type' // Errors // ------------------------------------------------------------------ // thrown externally +// prettier-ignore export class TransformDecodeCheckError extends TypeBoxError { - constructor(public readonly schema: TSchema, public readonly value: unknown, public readonly error: ValueError) { - super(`Unable to decode due to invalid value`) + constructor( + public readonly schema: TSchema, + public readonly value: unknown, + public readonly error: ValueError + ) { + super(`Unable to decode value as it does not match the expected schema`) } } +// prettier-ignore export class TransformDecodeError extends TypeBoxError { - constructor(public readonly schema: TSchema, public readonly value: unknown, error: any) { - super(`${error instanceof Error ? error.message : 'Unknown error'}`) + constructor( + public readonly schema: TSchema, + public readonly path: string, + public readonly value: unknown, + public readonly error: Error, + ) { + super(error instanceof Error ? error.message : 'Unknown error') } } // ------------------------------------------------------------------ // Decode // ------------------------------------------------------------------ // prettier-ignore -function Default(schema: TSchema, value: any) { +function Default(schema: TSchema, path: string, value: any) { try { return IsTransform(schema) ? schema[TransformKind].Decode(value) : value } catch (error) { - throw new TransformDecodeError(schema, value, error) + throw new TransformDecodeError(schema, path, value, error as Error) } } // prettier-ignore -function FromArray(schema: TArray, references: TSchema[], value: any): any { +function FromArray(schema: TArray, references: TSchema[], path: string, value: any): any { return (IsArray(value)) - ? Default(schema, value.map((value: any) => Visit(schema.items, references, value))) - : Default(schema, value) + ? Default(schema, path, value.map((value: any, index) => Visit(schema.items, references, `${path}/${index}`, value))) + : Default(schema, path, value) } // prettier-ignore -function FromIntersect(schema: TIntersect, references: TSchema[], value: any) { - if (!IsStandardObject(value) || IsValueType(value)) return Default(schema, value) +function FromIntersect(schema: TIntersect, references: TSchema[], path: string, value: any) { + if (!IsStandardObject(value) || IsValueType(value)) return Default(schema, path, value) const knownKeys = KeyOfPropertyKeys(schema) as string[] const knownProperties = knownKeys.reduce((value, key) => { return (key in value) - ? { ...value, [key]: Visit(Index(schema, [key]), references, value[key]) } + ? { ...value, [key]: Visit(Index(schema, [key]), references, `${path}/${key}`, value[key]) } : value }, value) if (!IsTransform(schema.unevaluatedProperties)) { - return Default(schema, knownProperties) + return Default(schema, path, knownProperties) } const unknownKeys = Object.getOwnPropertyNames(knownProperties) const unevaluatedProperties = schema.unevaluatedProperties as TSchema const unknownProperties = unknownKeys.reduce((value, key) => { return !knownKeys.includes(key) - ? { ...value, [key]: Default(unevaluatedProperties, value[key]) } + ? { ...value, [key]: Default(unevaluatedProperties, `${path}/${key}`, value[key]) } : value }, knownProperties) - return Default(schema, unknownProperties) + return Default(schema, path, unknownProperties) } -function FromNot(schema: TNot, references: TSchema[], value: any) { - return Default(schema, Visit(schema.not, references, value)) +function FromNot(schema: TNot, references: TSchema[], path: string, value: any) { + return Default(schema, path, Visit(schema.not, references, path, value)) } // prettier-ignore -function FromObject(schema: TObject, references: TSchema[], value: any) { - if (!IsStandardObject(value)) return Default(schema, value) +function FromObject(schema: TObject, references: TSchema[], path: string, value: any) { + if (!IsStandardObject(value)) return Default(schema, path, value) const knownKeys = KeyOfPropertyKeys(schema) const knownProperties = knownKeys.reduce((value, key) => { return (key in value) - ? { ...value, [key]: Visit(schema.properties[key], references, value[key]) } + ? { ...value, [key]: Visit(schema.properties[key], references, `${path}/${key}`, value[key]) } : value }, value) if (!IsSchema(schema.additionalProperties)) { - return Default(schema, knownProperties) + return Default(schema, path, knownProperties) } const unknownKeys = Object.getOwnPropertyNames(knownProperties) const additionalProperties = schema.additionalProperties as TSchema const unknownProperties = unknownKeys.reduce((value, key) => { return !knownKeys.includes(key) - ? { ...value, [key]: Default(additionalProperties, value[key]) } + ? { ...value, [key]: Default(additionalProperties, `${path}/${key}`, value[key]) } : value }, knownProperties) - return Default(schema, unknownProperties) + return Default(schema, path, unknownProperties) } // prettier-ignore -function FromRecord(schema: TRecord, references: TSchema[], value: any) { - if (!IsStandardObject(value)) return Default(schema, value) +function FromRecord(schema: TRecord, references: TSchema[], path: string, value: any) { + if (!IsStandardObject(value)) return Default(schema, path, value) const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] const knownKeys = new RegExp(pattern) const knownProperties = Object.getOwnPropertyNames(value).reduce((value, key) => { return knownKeys.test(key) - ? { ...value, [key]: Visit(schema.patternProperties[pattern], references, value[key]) } + ? { ...value, [key]: Visit(schema.patternProperties[pattern], references, `${path}/${key}`, value[key]) } : value }, value) if (!IsSchema(schema.additionalProperties)) { - return Default(schema, knownProperties) + return Default(schema, path, knownProperties) } const unknownKeys = Object.getOwnPropertyNames(knownProperties) const additionalProperties = schema.additionalProperties as TSchema const unknownProperties = unknownKeys.reduce((value, key) => { return !knownKeys.test(key) - ? { ...value, [key]: Default(additionalProperties, value[key]) } + ? { ...value, [key]: Default(additionalProperties, `${path}/${key}`, value[key]) } : value }, knownProperties) - return Default(schema, unknownProperties) + return Default(schema, path, unknownProperties) } // prettier-ignore -function FromRef(schema: TRef, references: TSchema[], value: any) { +function FromRef(schema: TRef, references: TSchema[], path: string, value: any) { const target = Deref(schema, references) - return Default(schema, Visit(target, references, value)) + return Default(schema, path, Visit(target, references, path, value)) } // prettier-ignore -function FromThis(schema: TThis, references: TSchema[], value: any) { +function FromThis(schema: TThis, references: TSchema[], path: string, value: any) { const target = Deref(schema, references) - return Default(schema, Visit(target, references, value)) + return Default(schema, path, Visit(target, references, path, value)) } // prettier-ignore -function FromTuple(schema: TTuple, references: TSchema[], value: any) { +function FromTuple(schema: TTuple, references: TSchema[], path: string, value: any) { return (IsArray(value) && IsArray(schema.items)) - ? Default(schema, schema.items.map((schema, index) => Visit(schema, references, value[index]))) - : Default(schema, value) + ? Default(schema, path, schema.items.map((schema, index) => Visit(schema, references, `${path}/${index}`, value[index]))) + : Default(schema, path, value) } // prettier-ignore -function FromUnion(schema: TUnion, references: TSchema[], value: any) { +function FromUnion(schema: TUnion, references: TSchema[], path: string, value: any) { for (const subschema of schema.anyOf) { if (!Check(subschema, references, value)) continue // note: ensure interior is decoded first - const decoded = Visit(subschema, references, value) - return Default(schema, decoded) + const decoded = Visit(subschema, references, path, value) + return Default(schema, path, decoded) } - return Default(schema, value) + return Default(schema, path, value) } // prettier-ignore -function Visit(schema: TSchema, references: TSchema[], value: any): any { +function Visit(schema: TSchema, references: TSchema[], path: string, value: any): any { const references_ = typeof schema.$id === 'string' ? [...references, schema] : references const schema_ = schema as any switch (schema[Kind]) { case 'Array': - return FromArray(schema_, references_, value) + return FromArray(schema_, references_, path, value) case 'Intersect': - return FromIntersect(schema_, references_, value) + return FromIntersect(schema_, references_, path, value) case 'Not': - return FromNot(schema_, references_, value) + return FromNot(schema_, references_, path, value) case 'Object': - return FromObject(schema_, references_, value) + return FromObject(schema_, references_, path, value) case 'Record': - return FromRecord(schema_, references_, value) + return FromRecord(schema_, references_, path, value) case 'Ref': - return FromRef(schema_, references_, value) + return FromRef(schema_, references_, path, value) case 'Symbol': - return Default(schema_, value) + return Default(schema_, path, value) case 'This': - return FromThis(schema_, references_, value) + return FromThis(schema_, references_, path, value) case 'Tuple': - return FromTuple(schema_, references_, value) + return FromTuple(schema_, references_, path, value) case 'Union': - return FromUnion(schema_, references_, value) + return FromUnion(schema_, references_, path, value) default: - return Default(schema_, value) + return Default(schema_, path, value) } } /** @@ -212,5 +223,5 @@ function Visit(schema: TSchema, references: TSchema[], value: any): any { * undefined behavior. Refer to the `Value.Decode()` for implementation details. */ export function TransformDecode(schema: TSchema, references: TSchema[], value: unknown): unknown { - return Visit(schema, references, value) + return Visit(schema, references, '', value) } diff --git a/src/value/transform/encode.ts b/src/value/transform/encode.ts index 695a7ea..2a2b8f4 100644 --- a/src/value/transform/encode.ts +++ b/src/value/transform/encode.ts @@ -56,13 +56,24 @@ import { IsTransform, IsSchema } from '../../type/guard/type' // ------------------------------------------------------------------ // Errors // ------------------------------------------------------------------ +// prettier-ignore export class TransformEncodeCheckError extends TypeBoxError { - constructor(public readonly schema: TSchema, public readonly value: unknown, public readonly error: ValueError) { - super(`Unable to encode due to invalid value`) + constructor( + public readonly schema: TSchema, + public readonly value: unknown, + public readonly error: ValueError + ) { + super(`The encoded value does not match the expected schema`) } } +// prettier-ignore export class TransformEncodeError extends TypeBoxError { - constructor(public readonly schema: TSchema, public readonly value: unknown, error: any) { + constructor( + public readonly schema: TSchema, + public readonly path: string, + public readonly value: unknown, + public readonly error: Error, + ) { super(`${error instanceof Error ? error.message : 'Unknown error'}`) } } @@ -70,53 +81,53 @@ export class TransformEncodeError extends TypeBoxError { // Encode // ------------------------------------------------------------------ // prettier-ignore -function Default(schema: TSchema, value: any) { +function Default(schema: TSchema, path: string, value: any) { try { return IsTransform(schema) ? schema[TransformKind].Encode(value) : value } catch (error) { - throw new TransformEncodeError(schema, value, error) + throw new TransformEncodeError(schema, path, value, error as Error) } } // prettier-ignore -function FromArray(schema: TArray, references: TSchema[], value: any): any { - const defaulted = Default(schema, value) +function FromArray(schema: TArray, references: TSchema[], path: string, value: any): any { + const defaulted = Default(schema, path, value) return IsArray(defaulted) - ? defaulted.map((value: any) => Visit(schema.items, references, value)) + ? defaulted.map((value: any, index) => Visit(schema.items, references, `${path}/${index}`, value)) : defaulted } // prettier-ignore -function FromIntersect(schema: TIntersect, references: TSchema[], value: any) { - const defaulted = Default(schema, value) +function FromIntersect(schema: TIntersect, references: TSchema[], path: string, value: any) { + const defaulted = Default(schema, path, value) if (!IsStandardObject(value) || IsValueType(value)) return defaulted const knownKeys = KeyOfPropertyKeys(schema) as string[] const knownProperties = knownKeys.reduce((value, key) => { return key in defaulted - ? { ...value, [key]: Visit(Index(schema, [key]), references, value[key]) } + ? { ...value, [key]: Visit(Index(schema, [key]), references, `${path}/${key}`, value[key]) } : value }, defaulted) if (!IsTransform(schema.unevaluatedProperties)) { - return Default(schema, knownProperties) + return Default(schema, path, knownProperties) } const unknownKeys = Object.getOwnPropertyNames(knownProperties) const unevaluatedProperties = schema.unevaluatedProperties as TSchema return unknownKeys.reduce((value, key) => { return !knownKeys.includes(key) - ? { ...value, [key]: Default(unevaluatedProperties, value[key]) } + ? { ...value, [key]: Default(unevaluatedProperties, `${path}/${key}`, value[key]) } : value }, knownProperties) } // prettier-ignore -function FromNot(schema: TNot, references: TSchema[], value: any) { - return Default(schema.not, Default(schema, value)) +function FromNot(schema: TNot, references: TSchema[], path: string, value: any) { + return Default(schema.not, path, Default(schema, path, value)) } // prettier-ignore -function FromObject(schema: TObject, references: TSchema[], value: any) { - const defaulted = Default(schema, value) +function FromObject(schema: TObject, references: TSchema[], path: string, value: any) { + const defaulted = Default(schema, path, value) if (!IsStandardObject(value)) return defaulted const knownKeys = KeyOfPropertyKeys(schema) as string[] const knownProperties = knownKeys.reduce((value, key) => { return key in value - ? { ...value, [key]: Visit(schema.properties[key], references, value[key]) } + ? { ...value, [key]: Visit(schema.properties[key], references, `${path}/${key}`, value[key]) } : value }, defaulted) if (!IsSchema(schema.additionalProperties)) { @@ -126,90 +137,90 @@ function FromObject(schema: TObject, references: TSchema[], value: any) { const additionalProperties = schema.additionalProperties as TSchema return unknownKeys.reduce((value, key) => { return !knownKeys.includes(key) - ? { ...value, [key]: Default(additionalProperties, value[key]) } + ? { ...value, [key]: Default(additionalProperties, `${path}/${key}`, value[key]) } : value }, knownProperties) } // prettier-ignore -function FromRecord(schema: TRecord, references: TSchema[], value: any) { - const defaulted = Default(schema, value) as Record +function FromRecord(schema: TRecord, references: TSchema[], path: string, value: any) { + const defaulted = Default(schema, path, value) as Record if (!IsStandardObject(value)) return defaulted const pattern = Object.getOwnPropertyNames(schema.patternProperties)[0] const knownKeys = new RegExp(pattern) const knownProperties = Object.getOwnPropertyNames(value).reduce((value, key) => { return knownKeys.test(key) - ? { ...value, [key]: Visit(schema.patternProperties[pattern], references, value[key]) } + ? { ...value, [key]: Visit(schema.patternProperties[pattern], references, `${path}/${key}`, value[key]) } : value }, defaulted) if (!IsSchema(schema.additionalProperties)) { - return Default(schema, knownProperties) + return Default(schema, path, knownProperties) } const unknownKeys = Object.getOwnPropertyNames(knownProperties) const additionalProperties = schema.additionalProperties as TSchema return unknownKeys.reduce((value, key) => { return !knownKeys.test(key) - ? { ...value, [key]: Default(additionalProperties, value[key]) } + ? { ...value, [key]: Default(additionalProperties, `${path}/${key}`, value[key]) } : value }, knownProperties) } // prettier-ignore -function FromRef(schema: TRef, references: TSchema[], value: any) { +function FromRef(schema: TRef, references: TSchema[], path: string, value: any) { const target = Deref(schema, references) - const resolved = Visit(target, references, value) - return Default(schema, resolved) + const resolved = Visit(target, references, path, value) + return Default(schema, path, resolved) } // prettier-ignore -function FromThis(schema: TThis, references: TSchema[], value: any) { +function FromThis(schema: TThis, references: TSchema[], path: string, value: any) { const target = Deref(schema, references) - const resolved = Visit(target, references, value) - return Default(schema, resolved) + const resolved = Visit(target, references, path, value) + return Default(schema, path, resolved) } // prettier-ignore -function FromTuple(schema: TTuple, references: TSchema[], value: any) { - const value1 = Default(schema, value) - return IsArray(schema.items) ? schema.items.map((schema, index) => Visit(schema, references, value1[index])) : [] +function FromTuple(schema: TTuple, references: TSchema[], path: string, value: any) { + const value1 = Default(schema, path, value) + return IsArray(schema.items) ? schema.items.map((schema, index) => Visit(schema, references, `${path}/${index}`, value1[index])) : [] } // prettier-ignore -function FromUnion(schema: TUnion, references: TSchema[], value: any) { +function FromUnion(schema: TUnion, references: TSchema[], path: string, value: any) { // test value against union variants for (const subschema of schema.anyOf) { if (!Check(subschema, references, value)) continue - const value1 = Visit(subschema, references, value) - return Default(schema, value1) + const value1 = Visit(subschema, references, path, value) + return Default(schema, path, value1) } // test transformed value against union variants for (const subschema of schema.anyOf) { - const value1 = Visit(subschema, references, value) + const value1 = Visit(subschema, references, path, value) if (!Check(schema, references, value1)) continue - return Default(schema, value1) + return Default(schema, path, value1) } - return Default(schema, value) + return Default(schema, path, value) } // prettier-ignore -function Visit(schema: TSchema, references: TSchema[], value: any): any { +function Visit(schema: TSchema, references: TSchema[], path: string, value: any): any { const references_ = typeof schema.$id === 'string' ? [...references, schema] : references const schema_ = schema as any switch (schema[Kind]) { case 'Array': - return FromArray(schema_, references_, value) + return FromArray(schema_, references_, path, value) case 'Intersect': - return FromIntersect(schema_, references_, value) + return FromIntersect(schema_, references_, path, value) case 'Not': - return FromNot(schema_, references_, value) + return FromNot(schema_, references_, path, value) case 'Object': - return FromObject(schema_, references_, value) + return FromObject(schema_, references_, path, value) case 'Record': - return FromRecord(schema_, references_, value) + return FromRecord(schema_, references_, path, value) case 'Ref': - return FromRef(schema_, references_, value) + return FromRef(schema_, references_, path, value) case 'This': - return FromThis(schema_, references_, value) + return FromThis(schema_, references_, path, value) case 'Tuple': - return FromTuple(schema_, references_, value) + return FromTuple(schema_, references_, path, value) case 'Union': - return FromUnion(schema_, references_, value) + return FromUnion(schema_, references_, path, value) default: - return Default(schema_, value) + return Default(schema_, path, value) } } /** @@ -219,5 +230,5 @@ function Visit(schema: TSchema, references: TSchema[], value: any): any { * `Value.Encode()` function for implementation details. */ export function TransformEncode(schema: TSchema, references: TSchema[], value: unknown): unknown { - return Visit(schema, references, value) + return Visit(schema, references, '', value) } diff --git a/src/value/value/value.ts b/src/value/value/value.ts index 6b5febc..f64dc18 100644 --- a/src/value/value/value.ts +++ b/src/value/value/value.ts @@ -113,7 +113,7 @@ export function Encode>(schema: T, value: 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()!) + if (!Check(schema, references, encoded)) throw new TransformEncodeCheckError(schema, encoded, Errors(schema, references, encoded).First()!) return encoded } /** Returns an iterator for each error in this value. */ diff --git a/test/runtime/type/guard/composite.ts b/test/runtime/type/guard/composite.ts index d3e9557..351041b 100644 --- a/test/runtime/type/guard/composite.ts +++ b/test/runtime/type/guard/composite.ts @@ -130,8 +130,9 @@ describe('type/guard/TComposite', () => { // ---------------------------------------------------------------- // Union // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/789 // prettier-ignore - it('Should composite Union 1', () => { + it('Should composite Union 1 (non-overlapping)', () => { const T = Type.Composite([ Type.Union([ Type.Object({ x: Type.Number() }), @@ -142,11 +143,12 @@ describe('type/guard/TComposite', () => { ]) ]) Assert.IsEqual(T, Type.Object({ - z: Type.Intersect([Type.Union([Type.Never(), Type.Never()]), Type.Number()]) + z: Type.Number() })) }) + // https://github.com/sinclairzx81/typebox/issues/789 // prettier-ignore - it('Should composite Union 2', () => { + it('Should composite Union 2 (overlapping)', () => { const T = Type.Composite([ Type.Union([ Type.Object({ x: Type.Number() }), diff --git a/test/runtime/type/guard/record.ts b/test/runtime/type/guard/record.ts index 89ec3df..f0c11b6 100644 --- a/test/runtime/type/guard/record.ts +++ b/test/runtime/type/guard/record.ts @@ -139,4 +139,18 @@ describe('type/guard/TRecord', () => { const R = TypeGuard.IsObject(Type.Record(K, Type.Number())) Assert.IsTrue(R) }) + // ------------------------------------------------------------------ + // Evaluated: Dollar Sign Escape + // https://github.com/sinclairzx81/typebox/issues/794 + // ------------------------------------------------------------------ + // prettier-ignore + { + const K = Type.TemplateLiteral('$prop${A|B|C}') // issue + const T = Type.Record(K, Type.String()) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.$propA)) + Assert.IsTrue(TypeGuard.IsString(T.properties.$propB)) + Assert.IsTrue(TypeGuard.IsString(T.properties.$propC)) + Assert.IsEqual(T.required, ['$propA', '$propB', '$propC']) + } }) diff --git a/test/runtime/type/template-literal/generate.ts b/test/runtime/type/template-literal/generate.ts index 2178571..1977547 100644 --- a/test/runtime/type/template-literal/generate.ts +++ b/test/runtime/type/template-literal/generate.ts @@ -44,12 +44,12 @@ describe('type/template-literal/TemplateLiteralExpressionGenerate', () => { it('Expression 2', () => { const E = TemplateLiteralParse('\\)') const R = [...TemplateLiteralExpressionGenerate(E)] - Assert.IsEqual(R, ['\\)']) + Assert.IsEqual(R, [')']) }) it('Expression 3', () => { const E = TemplateLiteralParse('\\(') const R = [...TemplateLiteralExpressionGenerate(E)] - Assert.IsEqual(R, ['\\(']) + Assert.IsEqual(R, ['(']) }) it('Expression 4', () => { const E = TemplateLiteralParse('') @@ -196,4 +196,9 @@ describe('type/template-literal/TemplateLiteralExpressionGenerate', () => { const R = [...TemplateLiteralExpressionGenerate(E)] Assert.IsEqual(R, ['011', '001']) }) + it('Expression 33', () => { + const E = TemplateLiteralParse('\\$prop(1|2|3)') + const R = [...TemplateLiteralExpressionGenerate(E)] + Assert.IsEqual(R, ['$prop1', '$prop2', '$prop3']) + }) }) diff --git a/test/runtime/type/template-literal/parse.ts b/test/runtime/type/template-literal/parse.ts index 57db98f..679cb4b 100644 --- a/test/runtime/type/template-literal/parse.ts +++ b/test/runtime/type/template-literal/parse.ts @@ -84,14 +84,14 @@ describe('type/template-literal/TemplateLiteralParser', () => { const E = TemplateLiteralParse('\\)') Assert.IsEqual(E, { type: 'const', - const: '\\)', + const: ')', }) }) it('Expression 3', () => { const E = TemplateLiteralParse('\\(') Assert.IsEqual(E, { type: 'const', - const: '\\(', + const: '(', }) }) it('Expression 4', () => { diff --git a/test/runtime/value/convert/intersect.ts b/test/runtime/value/convert/intersect.ts index 5c2e2bb..191150a 100644 --- a/test/runtime/value/convert/intersect.ts +++ b/test/runtime/value/convert/intersect.ts @@ -12,7 +12,10 @@ describe('value/convert/Intersect', () => { const R = Value.Convert(T, { x: '1', y: '2' }) Assert.IsEqual(R, { x: 1, y: 2 }) }) - it('Should not convert for non object exclusive intersect', () => { + // ---------------------------------------------------------------- + // Intersection Complex + // ---------------------------------------------------------------- + it('Should complex intersect 1', () => { // prettier-ignore const T = Type.Intersect([ Type.Number(), @@ -20,9 +23,19 @@ describe('value/convert/Intersect', () => { Type.Object({ y: Type.Number() }) ]) const R = Value.Convert(T, { x: '1', y: '2' }) - Assert.IsEqual(R, { x: '1', y: '2' }) + Assert.IsEqual(R, { x: 1, y: 2 }) }) - it('Should convert first type for object exclusive intersect 1', () => { + it('Should complex intersect 2', () => { + // prettier-ignore + const T = Type.Intersect([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }), + Type.Number(), + ]) + const R = Value.Convert(T, { x: '3', y: '4' }) + Assert.IsEqual(R, { x: 3, y: 4 }) + }) + it('Should complex intersect 3', () => { // prettier-ignore const T = Type.Intersect([ Type.Number(), @@ -32,14 +45,4 @@ describe('value/convert/Intersect', () => { const R = Value.Convert(T, '123') Assert.IsEqual(R, 123) }) - it('Should convert first type for object exclusive intersect 2', () => { - // prettier-ignore - const T = Type.Intersect([ - Type.Object({ x: Type.Number() }), - Type.Object({ y: Type.Number() }), - Type.Number(), - ]) - const R = Value.Convert(T, { x: '3', y: '4' }) - Assert.IsEqual(R, { x: 3, y: '4' }) - }) }) diff --git a/test/runtime/value/convert/union.ts b/test/runtime/value/convert/union.ts index 12f731b..227e76c 100644 --- a/test/runtime/value/convert/union.ts +++ b/test/runtime/value/convert/union.ts @@ -1,4 +1,4 @@ -import { Value } from '@sinclair/typebox/value' +import { Convert, Value } from '@sinclair/typebox/value' import { Type } from '@sinclair/typebox' import { Assert } from '../../assert/index' @@ -14,11 +14,30 @@ describe('value/convert/Union', () => { Assert.IsEqual(V2, { x: null }) Assert.IsEqual(V3, { x: 'hello' }) }) - it('Should convert first variant in ambiguous conversion', () => { + it('Should convert last variant in ambiguous conversion', () => { const T = Type.Object({ x: Type.Union([Type.Boolean(), Type.Number()]), }) const V1 = Value.Convert(T, { x: '1' }) - Assert.IsEqual(V1, { x: true }) + Assert.IsEqual(V1, { x: 1 }) + }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/787 + // ---------------------------------------------------------------- + // prettier-ignore + it('Should convert Intersect Union', () => { + const T = Type.Intersect([ + Type.Union([ + Type.Object({ a: Type.Number() }), + Type.Object({ b: Type.Number() }), + ]), + Type.Object({ c: Type.Number() }), + ]) + const A = Convert(T, { a: '1', c: '2' }) + const B = Convert(T, { b: '1', c: '2' }) + const C = Convert(T, { a: '1', b: '2', c: '3' }) + Assert.IsEqual(A, { a: 1, c: 2 }) + Assert.IsEqual(B, { b: 1, c: 2 }) + Assert.IsEqual(C, { a: 1, b: 2, c: 3 }) }) }) diff --git a/test/static/composite.ts b/test/static/composite.ts index e2b440f..f124f6d 100644 --- a/test/static/composite.ts +++ b/test/static/composite.ts @@ -1,5 +1,5 @@ import { Expect } from './assert' -import { Type, TOptional, TObject, TIntersect, TNumber, TBoolean } from '@sinclair/typebox' +import { Type, TOptional, TObject, TUnion, TIntersect, TNumber, TString, TBoolean } from '@sinclair/typebox' // ---------------------------------------------------------------------------- // Overlapping - Non Varying @@ -184,3 +184,30 @@ import { Type, TOptional, TObject, TIntersect, TNumber, TBoolean } from '@sincla ]) ]) } +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T: TObject<{ + x: TNumber; + }> = Type.Composite([ + Type.Union([ + Type.Object({ x: Type.Number() }), + Type.Object({ y: Type.Number() }) + ]), + Type.Object({ x: Type.Number() }) + ]) +} +// prettier-ignore +{ + const T: TObject<{ + x: TIntersect<[TUnion<[TString, TString]>, TNumber]>; + }> = Type.Composite([ + Type.Union([ + Type.Object({ x: Type.String() }), + Type.Object({ x: Type.String() }) + ]), + Type.Object({ x: Type.Number() }) + ]) +} diff --git a/test/static/record.ts b/test/static/record.ts index e284ce0..da8a028 100644 --- a/test/static/record.ts +++ b/test/static/record.ts @@ -175,3 +175,17 @@ import { Type, Static } from '@sinclair/typebox' const T = Type.Record(Type.Enum(E), Type.Number()) Expect(T).ToStatic<{ [x: string]: number }> } +// ------------------------------------------------------------------ +// Dollar Sign Escape +// https://github.com/sinclairzx81/typebox/issues/794 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const K = Type.TemplateLiteral('$prop${A|B|C}') // issue + const T = Type.Record(K, Type.String()) + Expect(T).ToStatic<{ + '$propA': string, + '$propB': string, + '$propC': string + }>() +} diff --git a/test/static/template-literal.ts b/test/static/template-literal.ts index 670b0b6..773ee1f 100644 --- a/test/static/template-literal.ts +++ b/test/static/template-literal.ts @@ -77,3 +77,38 @@ import { Type } from '@sinclair/typebox' const T = Type.TemplateLiteral([Type.Literal('hello'), A]) Expect(T).ToStatic<'helloA' | 'helloB'>() } +// ------------------------------------------------------------------ +// Dollar Sign Escape +// https://github.com/sinclairzx81/typebox/issues/794 +// ------------------------------------------------------------------ +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}') // issue + Expect(T).ToStatic<'$propA' | '$propB' | '$propC'>() +} +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}x') // trailing + Expect(T).ToStatic<'$propAx' | '$propBx' | '$propCx'>() +} +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}x}') // non-greedy + Expect(T).ToStatic<'$propAx}' | '$propBx}' | '$propCx}'>() +} +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}x}${X|Y}') // distributive - non-greedy + Expect(T).ToStatic< + '$propAx}X' | '$propBx}X' | '$propCx}X' | + '$propAx}Y' | '$propBx}Y' | '$propCx}Y' + >() +} +// prettier-ignore +{ + const T = Type.TemplateLiteral('$prop${A|B|C}x}${X|Y}x') // distributive - non-greedy - trailing + Expect(T).ToStatic< + '$propAx}Xx' | '$propBx}Xx' | '$propCx}Xx' | + '$propAx}Yx' | '$propBx}Yx' | '$propCx}Yx' + >() +}