diff --git a/src/compile/path.ts b/src/compile/path.ts new file mode 100644 index 0000000..0088efa --- /dev/null +++ b/src/compile/path.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------------------------------------------------------------------*/ + +/** + * Converts a json pointer path (https://datatracker.ietf.org/doc/html/rfc6901) + * to a format which is followed by standard-schema and zod + */ +export function ZodPathFromJsonPointer(jsonPointer: string): PropertyKey[] { + if (jsonPointer === '') return [] + if (jsonPointer === '/') return [''] + return jsonPointer + .substring(1) + .split('/') + .map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~')) + .map((segment) => (isNaN(segment as any) ? segment : parseInt(segment))) +} diff --git a/src/compile/validator.ts b/src/compile/validator.ts index c4c19f8..242480a 100644 --- a/src/compile/validator.ts +++ b/src/compile/validator.ts @@ -29,6 +29,7 @@ THE SOFTWARE. import { TypeCheck, ValueErrorIterator } from '@sinclair/typebox/compiler' import { Value } from '@sinclair/typebox/value' import * as t from '@sinclair/typebox' +import { ZodPathFromJsonPointer } from './path' import * as s from './standard' // ------------------------------------------------------------------ @@ -65,7 +66,7 @@ export class StandardSchemaProps // ---------------------------------------------------------------- private __createIssues(value: unknown) { const errors = [...Value.Errors(this.__check.Schema(), value)] - const issues: s.StandardSchemaV1.Issue[] = errors.map((error) => ({ ...error, path: [error.path] })) + const issues: s.StandardSchemaV1.Issue[] = errors.map((error) => ({ ...error, path: ZodPathFromJsonPointer(error.path) })) return { issues } } private __createValue(value: unknown) { diff --git a/test/compile.ts b/test/compile.ts index 4183abb..b234aeb 100644 --- a/test/compile.ts +++ b/test/compile.ts @@ -1,5 +1,6 @@ import { Assert } from './assert' import { TypeBox, Valibot, Zod, Compile } from '@sinclair/typemap' +import { ZodPathFromJsonPointer } from 'src/compile/path' describe('Compile', () => { // ---------------------------------------------------------------- @@ -53,3 +54,38 @@ describe('Compile', () => { Assert.IsTrue(R.issues!.length > 0) }) }) + +describe('ZodPathFromJsonPointer', () => { + it('Should convert empty string', () => { + const jsonPointer = '' + Assert.IsEqual(ZodPathFromJsonPointer(jsonPointer), []) + }) + it('Should convert whole object path', () => { + const jsonPointer = '/' + Assert.IsEqual(ZodPathFromJsonPointer(jsonPointer), ['']) + }) + it('Should convert nested object path', () => { + const jsonPointer = '/a/b' + Assert.IsEqual(ZodPathFromJsonPointer(jsonPointer), ['a', 'b']) + }) + it('Should convert array path', () => { + const jsonPointer = '/a/0' + Assert.IsEqual(ZodPathFromJsonPointer(jsonPointer), ['a', 0]) + }) + it('Should convert keys which looks like array index', () => { + const jsonPointer = '/a/"0"' + Assert.IsEqual(ZodPathFromJsonPointer(jsonPointer), ['a', '"0"']) + }) + it('Should convert keys with / escape', () => { + const jsonPointer = '/a~1b' + Assert.IsEqual(ZodPathFromJsonPointer(jsonPointer), ['a/b']) + }) + it('Should convert keys with ~ escape', () => { + const jsonPointer = '/m~0n' + Assert.IsEqual(ZodPathFromJsonPointer(jsonPointer), ['m~n']) + }) + it('Should convert keys with ~ and / escape', () => { + const jsonPointer = '/m~01n' + Assert.IsEqual(ZodPathFromJsonPointer(jsonPointer), ['m~1n']) + }) +})