Skip to content

Commit

Permalink
feat(json-pointer): use error hierarchy and metadata when throwing er…
Browse files Browse the repository at this point in the history
…rors (#3106)

Refs #3039
  • Loading branch information
char0n authored Sep 1, 2023
1 parent 78b8a15 commit a17f250
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 26 deletions.
4 changes: 2 additions & 2 deletions packages/apidom-core/src/elements/SourceMap.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions packages/apidom-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 14 additions & 4 deletions packages/apidom-json-pointer/src/compile.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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<T extends Element> extends ApiDOMErrorOptions {
readonly pointer: string;
readonly tokens?: string[];
readonly failedToken?: string;
readonly failedTokenPosition?: number;
readonly element: T;
}

class EvaluationJsonPointerError<T extends Element> 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<T>) {
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;
22 changes: 18 additions & 4 deletions packages/apidom-json-pointer/src/errors/InvalidJsonPointerError.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions packages/apidom-json-pointer/src/errors/JsonPointerError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApiDOMStructuredError } from '@swagger-api/apidom-error';

class JsonPointerError extends ApiDOMStructuredError {}

export default JsonPointerError;
2 changes: 0 additions & 2 deletions packages/apidom-json-pointer/src/errors/index.ts

This file was deleted.

45 changes: 39 additions & 6 deletions packages/apidom-json-pointer/src/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,64 @@ 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 = <T extends Element>(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);
}

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);
};

Expand Down
8 changes: 7 additions & 1 deletion packages/apidom-json-pointer/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
23 changes: 18 additions & 5 deletions packages/apidom-json-pointer/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] => {
Expand All @@ -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,
},
);
}
};

/**
Expand Down

0 comments on commit a17f250

Please sign in to comment.