From a17f25052db0d83f1ac3fcea94f2e58b50105df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Fri, 1 Sep 2023 12:23:31 +0200 Subject: [PATCH] feat(json-pointer): use error hierarchy and metadata when throwing errors (#3106) Refs #3039 --- .../apidom-core/src/elements/SourceMap.ts | 4 +- packages/apidom-core/src/index.ts | 1 + packages/apidom-json-pointer/src/compile.ts | 18 ++++++-- .../src/errors/CompilationJsonPointerError.ts | 21 +++++++++ .../src/errors/EvaluationJsonPointerError.ts | 46 ++++++++++++++++++- .../src/errors/InvalidJsonPointerError.ts | 22 +++++++-- .../src/errors/JsonPointerError.ts | 5 ++ .../apidom-json-pointer/src/errors/index.ts | 2 - packages/apidom-json-pointer/src/evaluate.ts | 45 +++++++++++++++--- packages/apidom-json-pointer/src/index.ts | 8 +++- packages/apidom-json-pointer/src/parse.ts | 23 ++++++++-- 11 files changed, 169 insertions(+), 26 deletions(-) create mode 100644 packages/apidom-json-pointer/src/errors/CompilationJsonPointerError.ts create mode 100644 packages/apidom-json-pointer/src/errors/JsonPointerError.ts delete mode 100644 packages/apidom-json-pointer/src/errors/index.ts diff --git a/packages/apidom-core/src/elements/SourceMap.ts b/packages/apidom-core/src/elements/SourceMap.ts index 22cb2420b..bc4087d29 100644 --- a/packages/apidom-core/src/elements/SourceMap.ts +++ b/packages/apidom-core/src/elements/SourceMap.ts @@ -1,12 +1,12 @@ import { ArrayElement, Element, Attributes, Meta } from 'minim'; -interface Position { +export interface Position { row: number; column: number; char: number; } -interface PositionRange { +export interface PositionRange { start: Position; end: Position; } diff --git a/packages/apidom-core/src/index.ts b/packages/apidom-core/src/index.ts index d5fec1faa..54ecd6424 100644 --- a/packages/apidom-core/src/index.ts +++ b/packages/apidom-core/src/index.ts @@ -15,6 +15,7 @@ export { default as MediaTypes } from './media-types'; export { Element, MemberElement, KeyValuePair, ObjectSlice, ArraySlice, refract } from 'minim'; export type { NamespacePluginOptions, Attributes, Meta } from 'minim'; +export type { PositionRange, Position } from './elements/SourceMap'; export { default as namespace, Namespace, createNamespace } from './namespace'; export { diff --git a/packages/apidom-json-pointer/src/compile.ts b/packages/apidom-json-pointer/src/compile.ts index c0550f5dc..7a07d1443 100644 --- a/packages/apidom-json-pointer/src/compile.ts +++ b/packages/apidom-json-pointer/src/compile.ts @@ -1,12 +1,22 @@ import escape from './escape'; +import CompilationJsonPointerError from './errors/CompilationJsonPointerError'; // compile :: String[] -> String const compile = (tokens: string[]): string => { - if (tokens.length === 0) { - return ''; - } + try { + if (tokens.length === 0) { + return ''; + } - return `/${tokens.map(escape).join('/')}`; + return `/${tokens.map(escape).join('/')}`; + } catch (error: unknown) { + throw new CompilationJsonPointerError( + 'JSON Pointer compilation of tokens encountered an error.', + { + tokens, + }, + ); + } }; export default compile; diff --git a/packages/apidom-json-pointer/src/errors/CompilationJsonPointerError.ts b/packages/apidom-json-pointer/src/errors/CompilationJsonPointerError.ts new file mode 100644 index 000000000..698d970ba --- /dev/null +++ b/packages/apidom-json-pointer/src/errors/CompilationJsonPointerError.ts @@ -0,0 +1,21 @@ +import { ApiDOMErrorOptions } from '@swagger-api/apidom-error'; + +import JsonPointerError from './JsonPointerError'; + +export interface CompilationJsonPointerErrorOptions extends ApiDOMErrorOptions { + readonly tokens: string[]; +} + +class CompilationJsonPointerError extends JsonPointerError { + public readonly tokens!: string[]; + + constructor(message?: string, structuredOptions?: CompilationJsonPointerErrorOptions) { + super(message, structuredOptions); + + if (typeof structuredOptions !== 'undefined') { + this.tokens = [...structuredOptions.tokens]; + } + } +} + +export default CompilationJsonPointerError; diff --git a/packages/apidom-json-pointer/src/errors/EvaluationJsonPointerError.ts b/packages/apidom-json-pointer/src/errors/EvaluationJsonPointerError.ts index 605a2b386..b5d60a8bd 100644 --- a/packages/apidom-json-pointer/src/errors/EvaluationJsonPointerError.ts +++ b/packages/apidom-json-pointer/src/errors/EvaluationJsonPointerError.ts @@ -1,3 +1,45 @@ -import { ApiDOMError } from '@swagger-api/apidom-error'; +import { ApiDOMErrorOptions } from '@swagger-api/apidom-error'; +import { Element, hasElementSourceMap, toValue } from '@swagger-api/apidom-core'; -export default class EvaluationJsonPointerError extends ApiDOMError {} +import JsonPointerError from './JsonPointerError'; + +export interface EvaluationJsonPointerErrorOptions extends ApiDOMErrorOptions { + readonly pointer: string; + readonly tokens?: string[]; + readonly failedToken?: string; + readonly failedTokenPosition?: number; + readonly element: T; +} + +class EvaluationJsonPointerError extends JsonPointerError { + public readonly pointer!: string; + + public readonly tokens?: string[]; + + public readonly failedToken?: string; + + public readonly failedTokenPosition?: number; + + public readonly element!: string; + + public readonly elementSourceMap?: [[number, number, number], [number, number, number]]; + + constructor(message?: string, structuredOptions?: EvaluationJsonPointerErrorOptions) { + super(message, structuredOptions); + + if (typeof structuredOptions !== 'undefined') { + this.pointer = structuredOptions.pointer; + if (Array.isArray(structuredOptions.tokens)) { + this.tokens = [...structuredOptions.tokens]; + } + this.failedToken = structuredOptions.failedToken; + this.failedTokenPosition = structuredOptions.failedTokenPosition; + this.element = structuredOptions.element.element; + if (hasElementSourceMap(structuredOptions.element)) { + this.elementSourceMap = toValue(structuredOptions.element.getMetaProperty('sourceMap')); + } + } + } +} + +export default EvaluationJsonPointerError; diff --git a/packages/apidom-json-pointer/src/errors/InvalidJsonPointerError.ts b/packages/apidom-json-pointer/src/errors/InvalidJsonPointerError.ts index 001683e37..6c175d9a1 100644 --- a/packages/apidom-json-pointer/src/errors/InvalidJsonPointerError.ts +++ b/packages/apidom-json-pointer/src/errors/InvalidJsonPointerError.ts @@ -1,7 +1,21 @@ -import { ApiDOMError } from '@swagger-api/apidom-error'; +import { ApiDOMErrorOptions } from '@swagger-api/apidom-error'; -export default class InvalidJsonPointerError extends ApiDOMError { - constructor(pointer: string) { - super(`Invalid JSON Pointer "${pointer}". Pointers must begin with "/"`); +import JsonPointerError from './JsonPointerError'; + +export interface InvalidJsonPointerErrorOptions extends ApiDOMErrorOptions { + readonly pointer: string; +} + +class InvalidJsonPointerError extends JsonPointerError { + public readonly pointer!: string; + + constructor(message?: string, structuredOptions?: InvalidJsonPointerErrorOptions) { + super(message, structuredOptions); + + if (typeof structuredOptions !== 'undefined') { + this.pointer = structuredOptions.pointer; + } } } + +export default InvalidJsonPointerError; diff --git a/packages/apidom-json-pointer/src/errors/JsonPointerError.ts b/packages/apidom-json-pointer/src/errors/JsonPointerError.ts new file mode 100644 index 000000000..1f15aa0ee --- /dev/null +++ b/packages/apidom-json-pointer/src/errors/JsonPointerError.ts @@ -0,0 +1,5 @@ +import { ApiDOMStructuredError } from '@swagger-api/apidom-error'; + +class JsonPointerError extends ApiDOMStructuredError {} + +export default JsonPointerError; diff --git a/packages/apidom-json-pointer/src/errors/index.ts b/packages/apidom-json-pointer/src/errors/index.ts deleted file mode 100644 index 21fd46029..000000000 --- a/packages/apidom-json-pointer/src/errors/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as EvaluationJsonPointerError } from './EvaluationJsonPointerError'; -export { default as InvalidJsonPointerError } from './InvalidJsonPointerError'; diff --git a/packages/apidom-json-pointer/src/evaluate.ts b/packages/apidom-json-pointer/src/evaluate.ts index 61d6c6e4b..e6425f02a 100644 --- a/packages/apidom-json-pointer/src/evaluate.ts +++ b/packages/apidom-json-pointer/src/evaluate.ts @@ -2,17 +2,38 @@ import { isInteger } from 'ramda-adjunct'; import { Element, isObjectElement, isArrayElement } from '@swagger-api/apidom-core'; import parse from './parse'; -import { EvaluationJsonPointerError } from './errors'; +import EvaluationJsonPointerError from './errors/EvaluationJsonPointerError'; // evaluates JSON Pointer against ApiDOM fragment const evaluate = (pointer: string, element: T): Element => { - const tokens = parse(pointer); + let tokens: string[]; - return tokens.reduce((acc, token) => { + try { + tokens = parse(pointer); + } catch (error: unknown) { + throw new EvaluationJsonPointerError( + `JSON Pointer evaluation failed while parsing the pointer "${pointer}".`, + { + pointer, + element, + }, + ); + } + + return tokens.reduce((acc, token, tokenPosition) => { if (isObjectElement(acc)) { // @ts-ignore if (!acc.hasKey(token)) { - throw new EvaluationJsonPointerError(`Evaluation failed on token: "${token}"`); + throw new EvaluationJsonPointerError( + `JSON Pointer evaluation failed while evaluating token "${token}" against an ObjectElement`, + { + pointer, + tokens, + failedToken: token, + failedTokenPosition: tokenPosition, + element: acc, + }, + ); } // @ts-ignore return acc.get(token); @@ -20,13 +41,25 @@ const evaluate = (pointer: string, element: T): Element => { if (isArrayElement(acc)) { if (!(token in acc.content) || !isInteger(Number(token))) { - throw new EvaluationJsonPointerError(`Evaluation failed on token: "${token}"`); + throw new EvaluationJsonPointerError( + `JSON Pointer evaluation failed while evaluating token "${token}" against an ArrayElement`, + { pointer, tokens, failedToken: token, failedTokenPosition: tokenPosition, element: acc }, + ); } // @ts-ignore return acc.get(Number(token)); } - throw new EvaluationJsonPointerError(`Evaluation failed on token: "${token}"`); + throw new EvaluationJsonPointerError( + `JSON Pointer evaluation failed while evaluating token "${token}" against an unexpected Element`, + { + pointer, + tokens, + failedToken: token, + failedTokenPosition: tokenPosition, + element: acc, + }, + ); }, element); }; diff --git a/packages/apidom-json-pointer/src/index.ts b/packages/apidom-json-pointer/src/index.ts index 5384fd738..23dfa8265 100644 --- a/packages/apidom-json-pointer/src/index.ts +++ b/packages/apidom-json-pointer/src/index.ts @@ -1,4 +1,10 @@ -export { InvalidJsonPointerError, EvaluationJsonPointerError } from './errors'; +export { default as JsonPointerError } from './errors/JsonPointerError'; +export { default as InvalidJsonPointerError } from './errors/InvalidJsonPointerError'; +export type { InvalidJsonPointerErrorOptions } from './errors/InvalidJsonPointerError'; +export { default as CompilationJsonPointerError } from './errors/CompilationJsonPointerError'; +export type { CompilationJsonPointerErrorOptions } from './errors/CompilationJsonPointerError'; +export { default as EvaluationJsonPointerError } from './errors/EvaluationJsonPointerError'; +export type { EvaluationJsonPointerErrorOptions } from './errors/EvaluationJsonPointerError'; export { default as escape } from './escape'; export { default as unescape } from './unescape'; export { default as parse, uriToPointer } from './parse'; diff --git a/packages/apidom-json-pointer/src/parse.ts b/packages/apidom-json-pointer/src/parse.ts index 294ae47b9..3cdf4de59 100644 --- a/packages/apidom-json-pointer/src/parse.ts +++ b/packages/apidom-json-pointer/src/parse.ts @@ -2,7 +2,7 @@ import { map, pipe, split, startsWith, tail } from 'ramda'; import { isEmptyString, trimCharsStart } from 'ramda-adjunct'; import unescape from './unescape'; -import { InvalidJsonPointerError } from './errors'; +import InvalidJsonPointerError from './errors/InvalidJsonPointerError'; // parse :: String -> String[] const parse = (pointer: string): string[] => { @@ -11,12 +11,25 @@ const parse = (pointer: string): string[] => { } if (!startsWith('/', pointer)) { - throw new InvalidJsonPointerError(pointer); + throw new InvalidJsonPointerError( + `Invalid JSON Pointer "${pointer}". JSON Pointers must begin with "/"`, + { + pointer, + }, + ); } - const tokens = pipe(split('/'), map(unescape))(pointer); - - return tail(tokens); + try { + const tokens = pipe(split('/'), map(unescape))(pointer); + return tail(tokens); + } catch (error: unknown) { + throw new InvalidJsonPointerError( + `JSON Pointer parsing of "${pointer}" encountered an error.`, + { + pointer, + }, + ); + } }; /**