From b0b66890008bab3b8fcaf94498f753bb72eaf8d5 Mon Sep 17 00:00:00 2001 From: sinclairzx81 Date: Thu, 26 Oct 2023 03:26:27 +0900 Subject: [PATCH] Revision 0.31.19 (#644) * Encode Error Path for RFC9601 Escape Sequences * Record Tests + Version --- package-lock.json | 4 +- package.json | 2 +- src/errors/errors.ts | 24 +-- test/runtime/errors/types/index.ts | 2 + .../errors/types/object-pointer-property.ts | 139 ++++++++++++++++++ .../errors/types/record-pointer-property.ts | 70 +++++++++ 6 files changed, 229 insertions(+), 12 deletions(-) create mode 100644 test/runtime/errors/types/object-pointer-property.ts create mode 100644 test/runtime/errors/types/record-pointer-property.ts diff --git a/package-lock.json b/package-lock.json index 12347b6..c831cfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.31.18", + "version": "0.31.19", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.31.18", + "version": "0.31.19", "license": "MIT", "devDependencies": { "@sinclair/hammer": "^0.18.0", diff --git a/package.json b/package.json index b3a03d8..85d102e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.31.18", + "version": "0.31.19", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/src/errors/errors.ts b/src/errors/errors.ts index f9b4b4e..9f09e1f 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -119,6 +119,12 @@ export class ValueErrorsUnknownTypeError extends Types.TypeBoxError { } } // -------------------------------------------------------------------------- +// EscapeKey +// -------------------------------------------------------------------------- +export function EscapeKey(key: string): string { + return key.replace(/~/g, '~0').replace(/\//g, '~1') // RFC6901 Path +} +// -------------------------------------------------------------------------- // Guards // -------------------------------------------------------------------------- function IsDefined(value: unknown): value is T { @@ -319,31 +325,31 @@ function* TObject(schema: Types.TObject, references: Types.TSchema[], path: stri const unknownKeys = Object.getOwnPropertyNames(value) for (const requiredKey of requiredKeys) { if (unknownKeys.includes(requiredKey)) continue - yield Create(ValueErrorType.ObjectRequiredProperty, schema.properties[requiredKey], `${path}/${requiredKey}`, undefined) + yield Create(ValueErrorType.ObjectRequiredProperty, schema.properties[requiredKey], `${path}/${EscapeKey(requiredKey)}`, undefined) } if (schema.additionalProperties === false) { for (const valueKey of unknownKeys) { if (!knownKeys.includes(valueKey)) { - yield Create(ValueErrorType.ObjectAdditionalProperties, schema, `${path}/${valueKey}`, value[valueKey]) + yield Create(ValueErrorType.ObjectAdditionalProperties, schema, `${path}/${EscapeKey(valueKey)}`, value[valueKey]) } } } if (typeof schema.additionalProperties === 'object') { for (const valueKey of unknownKeys) { if (knownKeys.includes(valueKey)) continue - yield* Visit(schema.additionalProperties as Types.TSchema, references, `${path}/${valueKey}`, value[valueKey]) + yield* Visit(schema.additionalProperties as Types.TSchema, references, `${path}/${EscapeKey(valueKey)}`, value[valueKey]) } } for (const knownKey of knownKeys) { const property = schema.properties[knownKey] if (schema.required && schema.required.includes(knownKey)) { - yield* Visit(property, references, `${path}/${knownKey}`, value[knownKey]) + yield* Visit(property, references, `${path}/${EscapeKey(knownKey)}`, value[knownKey]) if (Types.ExtendsUndefined.Check(schema) && !(knownKey in value)) { - yield Create(ValueErrorType.ObjectRequiredProperty, property, `${path}/${knownKey}`, undefined) + yield Create(ValueErrorType.ObjectRequiredProperty, property, `${path}/${EscapeKey(knownKey)}`, undefined) } } else { if (TypeSystemPolicy.IsExactOptionalProperty(value, knownKey)) { - yield* Visit(property, references, `${path}/${knownKey}`, value[knownKey]) + yield* Visit(property, references, `${path}/${EscapeKey(knownKey)}`, value[knownKey]) } } } @@ -362,17 +368,17 @@ function* TRecord(schema: Types.TRecord, references: Types.TSchema[], path: stri const [patternKey, patternSchema] = Object.entries(schema.patternProperties)[0] const regex = new RegExp(patternKey) for (const [propertyKey, propertyValue] of Object.entries(value)) { - if (regex.test(propertyKey)) yield* Visit(patternSchema, references, `${path}/${propertyKey}`, propertyValue) + if (regex.test(propertyKey)) yield* Visit(patternSchema, references, `${path}/${EscapeKey(propertyKey)}`, propertyValue) } if (typeof schema.additionalProperties === 'object') { for (const [propertyKey, propertyValue] of Object.entries(value)) { - if (!regex.test(propertyKey)) yield* Visit(schema.additionalProperties as Types.TSchema, references, `${path}/${propertyKey}`, propertyValue) + if (!regex.test(propertyKey)) yield* Visit(schema.additionalProperties as Types.TSchema, references, `${path}/${EscapeKey(propertyKey)}`, propertyValue) } } if (schema.additionalProperties === false) { for (const [propertyKey, propertyValue] of Object.entries(value)) { if (regex.test(propertyKey)) continue - return yield Create(ValueErrorType.ObjectAdditionalProperties, schema, `${path}/${propertyKey}`, propertyValue) + return yield Create(ValueErrorType.ObjectAdditionalProperties, schema, `${path}/${EscapeKey(propertyKey)}`, propertyValue) } } } diff --git a/test/runtime/errors/types/index.ts b/test/runtime/errors/types/index.ts index 077ccbe..44730ba 100644 --- a/test/runtime/errors/types/index.ts +++ b/test/runtime/errors/types/index.ts @@ -43,9 +43,11 @@ import './number-multiple-of' import './object-additional-properties' import './object-max-properties' import './object-min-properties' +import './object-pointer-property' import './object-required-property' import './object' import './promise' +import './record-pointer-property' import './string-format-unknown' import './string-format' import './string-max-length' diff --git a/test/runtime/errors/types/object-pointer-property.ts b/test/runtime/errors/types/object-pointer-property.ts new file mode 100644 index 0000000..ccf0458 --- /dev/null +++ b/test/runtime/errors/types/object-pointer-property.ts @@ -0,0 +1,139 @@ +import { Type } from '@sinclair/typebox' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/ObjectPointerProperty', () => { + // ---------------------------------------------------------------- + // Known + // ---------------------------------------------------------------- + it('Should produce known pointer property path 1', () => { + const T = Type.Object({ 'a/b': Type.String() }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce known pointer property path 2', () => { + const T = Type.Object({ 'a~b': Type.String() }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + it('Should produce known pointer property path 3', () => { + const T = Type.Object({ 'a/b~c': Type.String() }) + const R = Resolve(T, { 'a/b~c': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b~0c') + }) + it('Should produce known pointer property path 4', () => { + const T = Type.Object({ 'a~b/c': Type.String() }) + const R = Resolve(T, { 'a~b/c': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b~1c') + }) + it('Should produce known pointer property path 5', () => { + const T = Type.Object({ 'a~b/c/d': Type.String() }) + const R = Resolve(T, { 'a~b/c/d': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b~1c~1d') + }) + it('Should produce known pointer property path 6', () => { + const T = Type.Object({ 'a~b/c/d~e': Type.String() }) + const R = Resolve(T, { 'a~b/c/d~e': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b~1c~1d~0e') + }) + // ---------------------------------------------------------------- + // Unknown Additional + // ---------------------------------------------------------------- + it('Should produce unknown pointer property path 1', () => { + const T = Type.Object({}, { additionalProperties: false }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce unknown pointer property path 2', () => { + const T = Type.Object({}, { additionalProperties: false }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // Unknown Constrained + // ---------------------------------------------------------------- + it('Should produce unknown constrained pointer property path 1', () => { + const T = Type.Object({}, { additionalProperties: Type.String() }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce unknown constrained pointer property path 2', () => { + const T = Type.Object({}, { additionalProperties: Type.String() }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // Nested + // ---------------------------------------------------------------- + it('Should produce nested pointer 1', () => { + const T = Type.Object({ + 'x/y': Type.Object({ + z: Type.Object({ + w: Type.String(), + }), + }), + }) + const R = Resolve(T, { 'x/y': { z: { w: 1 } } }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x~1y/z/w') + }) + it('Should produce nested pointer 2', () => { + const T = Type.Object({ + x: Type.Object({ + 'y/z': Type.Object({ + w: Type.String(), + }), + }), + }) + const R = Resolve(T, { x: { 'y/z': { w: 1 } } }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x/y~1z/w') + }) + it('Should produce nested pointer 3', () => { + const T = Type.Object({ + x: Type.Object({ + y: Type.Object({ + 'z/w': Type.String(), + }), + }), + }) + const R = Resolve(T, { x: { y: { 'z/w': 1 } } }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x/y/z~1w') + }) + // ---------------------------------------------------------------- + // Nested Array + // ---------------------------------------------------------------- + it('Should produce nested array pointer property path 1', () => { + const T = Type.Object({ + 'x/y': Type.Object({ + z: Type.Array(Type.String()), + }), + }) + const R = Resolve(T, { 'x/y': { z: ['a', 'b', 1] } }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x~1y/z/2') + }) + it('Should produce nested array pointer property path 2', () => { + const T = Type.Object({ + x: Type.Array( + Type.Object({ + 'y/z': Type.String(), + }), + ), + }) + const R = Resolve(T, { x: [{ 'y/z': 'a' }, { 'y/z': 'b' }, { 'y/z': 1 }] }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x/2/y~1z') + }) +}) diff --git a/test/runtime/errors/types/record-pointer-property.ts b/test/runtime/errors/types/record-pointer-property.ts new file mode 100644 index 0000000..a2dbd34 --- /dev/null +++ b/test/runtime/errors/types/record-pointer-property.ts @@ -0,0 +1,70 @@ +import { Type } from '@sinclair/typebox' +import { Resolve } from './resolve' +import { Assert } from '../../assert' + +describe('errors/type/RecordPointerProperty', () => { + // ---------------------------------------------------------------- + // Known + // ---------------------------------------------------------------- + it('Should produce known pointer property path 1', () => { + const T = Type.Record(Type.String(), Type.String()) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce known pointer property path 2', () => { + const T = Type.Record(Type.String(), Type.String()) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // Unknown + // ---------------------------------------------------------------- + it('Should produce unknown pointer property path 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: false, + }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce unknown pointer property path 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: false, + }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // Unknown Constrained + // ---------------------------------------------------------------- + it('Should produce unknown constrained pointer property path 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.String(), + }) + const R = Resolve(T, { 'a/b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~1b') + }) + it('Should produce unknown constrained pointer property path 1', () => { + const T = Type.Record(Type.Number(), Type.String(), { + additionalProperties: Type.String(), + }) + const R = Resolve(T, { 'a~b': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/a~0b') + }) + // ---------------------------------------------------------------- + // PatternProperties + // ---------------------------------------------------------------- + it('Should produce pattern pointer property path 1', () => { + const T = Type.Record(Type.TemplateLiteral('${string}/${string}/c'), Type.String(), { + additionalProperties: false, + }) + const R = Resolve(T, { 'x/y/z': 1 }) + Assert.IsEqual(R.length, 1) + Assert.IsEqual(R[0].path, '/x~1y~1z') + }) +})