Skip to content

Commit 9946d4b

Browse files
Initial implementation of JSON Schema Draft-4 and JSON Schema 2019-09
1 parent e5e7603 commit 9946d4b

21 files changed

+1895
-0
lines changed

src/anchor.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { File } from "./file";
2+
import { JsonSchema } from "./json-schema";
3+
import { Pointer } from "./pointer";
4+
import { Resource } from "./resource";
5+
import { createURL } from "./url";
6+
import { createError } from "./utils";
7+
8+
const A = 65, Z = 90, a = 97, z = 122, hash = 35;
9+
const namePattern = /^[a-z][a-z0-9_:.-]*$/i;
10+
const code = "ERR_INVALID_ANCHOR";
11+
12+
/**
13+
* The arguments that can be passed to the `Anchor` constructor
14+
*/
15+
export interface AnchorArgs {
16+
resource: Resource;
17+
locationInFile: string[];
18+
name: string | unknown;
19+
data: object;
20+
}
21+
22+
/**
23+
* A JSON Schema anchor
24+
*
25+
* @see http://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.8.2.3
26+
*/
27+
export class Anchor {
28+
/**
29+
* The JSON Schema that this anchor is part of
30+
*/
31+
public schema: JsonSchema;
32+
33+
/**
34+
* The file that contains this anchor
35+
*/
36+
public file: File;
37+
38+
/**
39+
* The JSON Schema resource that contains this anchor
40+
*/
41+
public resource: Resource;
42+
43+
/**
44+
* A pointer to the anchor's location in the file
45+
*/
46+
public locationInFile: Pointer;
47+
48+
/**
49+
* The name of this anchor (e.g. "my-anchor")
50+
*
51+
* NOTE: According to the 2019-09 spec, anchor names MUST start with a letter
52+
*/
53+
public name: string;
54+
55+
/**
56+
* The anchor object in the JSON Schema
57+
*/
58+
public data: object;
59+
60+
/**
61+
* The absolute URI of this anchor
62+
*/
63+
public uri: URL;
64+
65+
public constructor(args: AnchorArgs) {
66+
let input = args.name;
67+
68+
if (typeof input !== "string") {
69+
let type = typeof input;
70+
throw createError(TypeError, `$anchor must be a string, not ${type}.`, { code, type, input });
71+
}
72+
73+
if (input.length === 0) {
74+
throw createError(SyntaxError, "$anchor cannot be empty.", { code, input });
75+
}
76+
77+
let firstChar = input.charCodeAt(0);
78+
if (firstChar === hash) {
79+
throw createError(SyntaxError, "$anchor cannot start with a \"#\" character.", { code, input });
80+
}
81+
else if (!((firstChar >= A && firstChar <= Z) || (firstChar >= a && firstChar <= z))) {
82+
throw createError(SyntaxError, "$anchor must start with a letter.", { code, input });
83+
}
84+
85+
if (!namePattern.test(input)) {
86+
throw createError(SyntaxError, "$anchor contains illegal characters.", { code, input });
87+
}
88+
89+
this.schema = args.resource.schema;
90+
this.file = args.resource.file;
91+
this.resource = args.resource;
92+
this.locationInFile = new Pointer(args.locationInFile);
93+
this.name = input;
94+
this.data = args.data;
95+
this.uri = createURL("#" + input, this.resource.uri);
96+
}
97+
98+
/**
99+
* Returns the anchor URI
100+
*/
101+
public toString(): string {
102+
return `${this.uri || "anchor"}`;
103+
}
104+
}

src/errors/createErrorHandler.ts

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { File } from "../file";
2+
import { Pointer } from "../pointer";
3+
import { ErrorProps, Helpers } from "./index";
4+
import { SchemaError } from "./schema-error";
5+
6+
7+
export interface ErrorHandlerOptions {
8+
file: File;
9+
code: string;
10+
message?: string;
11+
continueOnError?: boolean;
12+
}
13+
14+
15+
/**
16+
* Returns a function that handles errors by either recording them or re-throwing them
17+
*/
18+
export function createErrorHandler(options: ErrorHandlerOptions): Helpers["handleError"] {
19+
let { file, code } = options;
20+
options.message = options.message || `Error in ${file}`;
21+
22+
return function handleError(error: SchemaError, arg2?: string | object, arg3?: object) {
23+
// Don't re-wrap errors that have already been handled.
24+
// (this happens if the error is caught again in a higher-level catch clause)
25+
if (!file.errors.includes(error)) {
26+
error = wrapError(error, arg2, arg3);
27+
file.errors.push(error);
28+
}
29+
30+
if (!options.continueOnError) {
31+
throw error;
32+
}
33+
};
34+
35+
/**
36+
* Wraps an error in a `SchemaError`
37+
*/
38+
function wrapError(error: SchemaError, arg2?: string | object, arg3?: object) {
39+
let message = "";
40+
let props: ErrorProps = {};
41+
42+
// Handle optional arguments and overloads
43+
if (typeof arg2 === "object") {
44+
props = arg2 as ErrorProps;
45+
}
46+
else {
47+
message = arg2 as string;
48+
if (arg3) {
49+
props = arg3 as ErrorProps;
50+
}
51+
}
52+
53+
let locationInFile = props.locationInFile ? new Pointer(props.locationInFile) : undefined;
54+
let originalError = props.originalError || error.originalError || error;
55+
56+
// Make sure originalError is the most deeply-nested error
57+
while ((originalError as ErrorProps).originalError) {
58+
originalError = (originalError as ErrorProps).originalError;
59+
}
60+
61+
if (!message) {
62+
// Create a default error message
63+
message = locationInFile ? `${options.message} at ${locationInFile}` : options.message!;
64+
}
65+
66+
message += `\n ${error.message}`;
67+
68+
return new SchemaError(Object.assign({}, originalError, error, props, {
69+
code, file, message, locationInFile, originalError
70+
}));
71+
}
72+
}

src/errors/index.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export * from "./createErrorHandler";
2+
export * from "./schema-error";
3+
4+
5+
/**
6+
* Additional information that can be added to an error when calling `handleError`
7+
*/
8+
export interface ErrorProps {
9+
code?: string;
10+
locationInFile?: string[];
11+
originalError?: Error | unknown;
12+
[key: string]: unknown;
13+
}
14+
15+
16+
/**
17+
* Helpful information and utilities that are passed to JSON Schema version implementations
18+
*/
19+
export interface Helpers {
20+
/**
21+
* Handles errors appropriately, based on user-specified options.
22+
* This may throw the error, or it may simply record the error and return.
23+
*
24+
* @param error - The error that occurred
25+
*/
26+
handleError(error: Error | unknown): void;
27+
28+
/**
29+
* Handles errors appropriately, based on user-specified options.
30+
* This may throw the error, or it may simply record the error and return.
31+
*
32+
* @param error - The error that occurred
33+
* @param props - Additional properties to add to the error when recording or re-throwing it
34+
*/
35+
handleError(error: Error | unknown, props: ErrorProps): void;
36+
37+
/**
38+
* Handles errors appropriately, based on user-specified options.
39+
* This may throw the error, or it may simply record the error and return.
40+
*
41+
* @param error - The error that occurred
42+
* @param message - A message to prepend to the original error's message
43+
* @param props - Additional properties to add to the error when recording or re-throwing it
44+
*/
45+
handleError(error: Error | unknown, message: string, props?: ErrorProps): void;
46+
}

src/errors/schema-error.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { File } from "../file";
2+
import { JsonSchema } from "../json-schema";
3+
import { Pointer } from "../pointer";
4+
5+
6+
/**
7+
* The arguments that can be passed to the `SchemaError` constructor
8+
*/
9+
export interface SchemaErrorArgs {
10+
code: string;
11+
file: File;
12+
message: string;
13+
locationInFile?: Pointer;
14+
originalError?: Error | unknown;
15+
[key: string]: unknown;
16+
}
17+
18+
19+
/**
20+
* An error in a JSON Schema
21+
*/
22+
export class SchemaError extends Error {
23+
/**
24+
* A code that indicates the type of error (e.g. ERR_INVALID_URL)
25+
*/
26+
public code!: string;
27+
28+
/**
29+
* The JSON schema, including all files that were successfully read
30+
*/
31+
public schema!: JsonSchema;
32+
33+
/**
34+
* The file that contains or caused this error
35+
*/
36+
public file!: File;
37+
38+
/**
39+
* The location in the file where the error occurred, if known
40+
*/
41+
public locationInFile?: Pointer;
42+
43+
/**
44+
* The original error that occurred
45+
*/
46+
public originalError?: Error;
47+
48+
public constructor(args: SchemaErrorArgs) {
49+
super(args.message);
50+
Object.assign(this, args, {
51+
schema: args.file.schema
52+
});
53+
this.name = "SchemaError";
54+
}
55+
}

0 commit comments

Comments
 (0)