Skip to content

Commit 0200144

Browse files
committed
completed the normalizingOutputFormat for betterJSONSchemaErrors
1 parent c2b7f23 commit 0200144

File tree

7 files changed

+543
-386
lines changed

7 files changed

+543
-386
lines changed

package-lock.json

Lines changed: 374 additions & 337 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@
3838
},
3939
"dependencies": {
4040
"@hyperjump/browser": "^1.3.1",
41-
"@hyperjump/json-schema": "^1.14.1"
41+
"@hyperjump/json-schema": "^1.16.0"
4242
}
4343
}

src/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const betterJsonSchemaErrors: (
22
instance: Json,
33
schema: SchemaObject,
44
errorOutput: OutputFormat
5-
) => BetterJsonSchemaErrors;
5+
) => Promise<BetterJsonSchemaErrors>;
66

77
export type BetterJsonSchemaErrors = {
88
errors: ErrorObject[];

src/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { normalizeOutputFormat } from "./normalizeOutputFormat/normalizeOutput.j
55
*/
66

77
/** @type betterJsonSchemaErrors */
8-
export function betterJsonSchemaErrors(instance, schema, errorOutput) {
9-
const normalizedErrors = normalizeOutputFormat(errorOutput);
8+
export async function betterJsonSchemaErrors(instance, schema, errorOutput) {
9+
const normalizedErrors = await normalizeOutputFormat(errorOutput, schema);
1010

1111
const errors = [];
1212
for (const error of normalizedErrors) {

src/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, test } from "vitest";
1+
import { describe, test } from "vitest";
22

33
describe("Better JSON Schema Errors", () => {
44
test("greeting", () => {
Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,61 @@
1+
import * as Browser from "@hyperjump/browser";
2+
import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12";
3+
import { getSchema } from "@hyperjump/json-schema/experimental";
4+
import { pointerSegments } from "@hyperjump/json-pointer";
5+
import { randomUUID } from "crypto";
6+
17
/**
2-
* @import {OutputFormat, OutputUnit, NormalizedError } from "../index.d.ts"
8+
* @import { OutputFormat, OutputUnit, NormalizedError, SchemaObject} from "../index.d.ts";
9+
* @import { SchemaDocument } from "@hyperjump/json-schema/experimental";
10+
* @import { Browser as BrowserType } from "@hyperjump/browser";
311
*/
412

5-
/** @type {(errorOutput: OutputFormat) => NormalizedError[]} */
6-
export function normalizeOutputFormat(errorOutput) {
7-
/** @type NormalizedError[] */
13+
/**
14+
* @param {OutputFormat} errorOutput
15+
* @param {SchemaObject} schema
16+
* @returns {Promise<NormalizedError[]>}
17+
*/
18+
export async function normalizeOutputFormat(errorOutput, schema) {
19+
/** @type {NormalizedError[]} */
820
const output = [];
21+
922
if (!errorOutput || errorOutput.valid !== false) {
1023
throw new Error("error Output must follow Draft 2019-09");
1124
}
1225

13-
/** @type {(errorOutput: OutputUnit) => void} */
14-
function collectErrors(error) {
26+
const keywords = new Set([
27+
"type", "minLength", "maxLength", "minimum", "maximum", "format", "pattern",
28+
"enum", "const", "required", "items", "properties", "allOf", "anyOf", "oneOf",
29+
"not", "contains", "uniqueItems", "additionalProperties", "minItems", "maxItems",
30+
"minProperties", "maxProperties", "dependentRequired", "dependencies"
31+
]);
32+
33+
/** @type {(errorOutput: OutputUnit) => Promise<void>} */
34+
async function collectErrors(error) {
1535
if (error.valid) return;
1636

1737
if (!("instanceLocation" in error) || !("absoluteKeywordLocation" in error || "keywordLocation" in error)) {
1838
throw new Error("error Output must follow Draft 2019-09");
1939
}
2040

21-
// TODO: Convert keywordLocation to absoluteKeywordLocation
22-
error.absoluteKeywordLocation ??= "https://example.com/main#/minLength";
41+
const absoluteKeywordLocation = error.absoluteKeywordLocation
42+
?? await toAbsoluteKeywordLocation(schema, /** @type string */ (error.keywordLocation));
43+
44+
const fragment = absoluteKeywordLocation.split("#")[1];
45+
const lastSegment = fragment.split("/").filter(Boolean).pop();
2346

24-
output.push({
25-
valid: false,
26-
absoluteKeywordLocation: error.absoluteKeywordLocation,
27-
instanceLocation: normalizeInstanceLocation(error.instanceLocation)
28-
});
47+
// make a check here to remove the schemaLocation.
48+
if (lastSegment && keywords.has(lastSegment)) {
49+
output.push({
50+
valid: false,
51+
absoluteKeywordLocation,
52+
instanceLocation: normalizeInstanceLocation(error.instanceLocation)
53+
});
54+
}
2955

3056
if (error.errors) {
3157
for (const nestedError of error.errors) {
32-
collectErrors(nestedError);
58+
await collectErrors(nestedError); // Recursive
3359
}
3460
}
3561
}
@@ -39,14 +65,35 @@ export function normalizeOutputFormat(errorOutput) {
3965
}
4066

4167
for (const err of errorOutput.errors) {
42-
collectErrors(err);
68+
await collectErrors(err);
4369
}
4470

4571
return output;
4672
}
4773

48-
/** @type (location: string) => string */
74+
/** @type {(location: string) => string} */
4975
function normalizeInstanceLocation(location) {
50-
if (location.startsWith("/") || location === "") return "#" + location;
51-
return location;
76+
return location.startsWith("/") || location === "" ? `#${location}` : location;
77+
}
78+
79+
/**
80+
* Convert keywordLocation to absoluteKeywordLocation
81+
* @param {SchemaObject} schema
82+
* @param {string} keywordLocation
83+
* @returns {Promise<string>}
84+
*/
85+
export async function toAbsoluteKeywordLocation(schema, keywordLocation) {
86+
const uri = `urn:uuid:${randomUUID()}`;
87+
try {
88+
registerSchema(schema, uri);
89+
90+
let browser = await getSchema(uri);
91+
for (const segment of pointerSegments(keywordLocation)) {
92+
browser = /** @type BrowserType<SchemaDocument> */ (await Browser.step(segment, browser));
93+
}
94+
95+
return `${browser.document.baseUri}#${browser.cursor}`;
96+
} finally {
97+
unregisterSchema(uri);
98+
}
5299
}

src/normalizeOutputFormat/normalizeOutput.test.js

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { describe, expect, test } from "vitest";
22
import { normalizeOutputFormat } from "./normalizeOutput.js";
33
import { betterJsonSchemaErrors } from "../index.js";
4-
54
/**
65
* @import { OutputFormat, OutputUnit, SchemaObject } from "../index.d.ts"
76
*/
87

98
describe("Error Output Normalization", () => {
10-
test("Simple keyword with a standard Basic output format", () => {
9+
test("Simple keyword with a standard Basic output format", async () => {
1110
/** @type SchemaObject */
1211
const schema = {
1312
minLength: 3
@@ -26,7 +25,7 @@ describe("Error Output Normalization", () => {
2625
]
2726
};
2827

29-
const result = betterJsonSchemaErrors(instance, schema, output);
28+
const result = await betterJsonSchemaErrors(instance, schema, output);
3029
expect(result.errors).to.eql([{
3130
schemaLocation: "https://example.com/main#/minLength",
3231
instanceLocation: "#",
@@ -35,9 +34,11 @@ describe("Error Output Normalization", () => {
3534
]);
3635
});
3736

38-
test("Checking when output contain only instanceLocation and keywordLocation ", () => {
37+
test("Checking when output contain only instanceLocation and keywordLocation ", async () => {
3938
/** @type SchemaObject */
4039
const schema = {
40+
$id: "https://example.com/main",
41+
$schema: "https://json-schema.org/draft/2020-12/schema",
4142
minLength: 3
4243
};
4344

@@ -54,17 +55,18 @@ describe("Error Output Normalization", () => {
5455
]
5556
};
5657

57-
const result = betterJsonSchemaErrors(instance, schema, output);
58+
const result = await betterJsonSchemaErrors(instance, schema, output);
5859
expect(result.errors).to.eql([{
5960
schemaLocation: "https://example.com/main#/minLength",
6061
instanceLocation: "#",
6162
message: "The instance should be at least 3 characters"
6263
}]);
6364
});
6465

65-
test("adding # if instanceLocation doesn't have it", () => {
66+
test("adding # if instanceLocation doesn't have it", async () => {
6667
/** @type SchemaObject */
6768
const schema = {
69+
$id: "https://example.com/main",
6870
minlength: 3
6971
};
7072

@@ -76,13 +78,13 @@ describe("Error Output Normalization", () => {
7678
errors: [
7779
{
7880
valid: false,
79-
keywordLocation: "",
81+
absoluteKeywordLocation: "https://example.com/main#/minLength",
8082
instanceLocation: ""
8183
}
8284
]
8385
};
8486

85-
const result = betterJsonSchemaErrors(instance, schema, output);
87+
const result = await betterJsonSchemaErrors(instance, schema, output);
8688
expect(result.errors).to.eql([{
8789
schemaLocation: "https://example.com/main#/minLength",
8890
instanceLocation: "#",
@@ -103,7 +105,10 @@ describe("Error Output Normalization", () => {
103105
// const absoluteKeywordLocation = "/$defs/foo/type";
104106
// const keywordLocation = "/properties/foo/$ref/type";
105107

106-
test("checking for the basic output format", () => {
108+
test("checking for the basic output format", async () => {
109+
const schema = {
110+
$id: "https://example.com/polygon"
111+
};
107112
const errorOutput = {
108113
valid: false,
109114
errors: [
@@ -131,12 +136,7 @@ describe("Error Output Normalization", () => {
131136
]
132137
};
133138

134-
expect(normalizeOutputFormat(errorOutput)).to.eql([
135-
{
136-
valid: false,
137-
absoluteKeywordLocation: "https://example.com/polygon#/$defs/point",
138-
instanceLocation: "#/1"
139-
},
139+
expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([
140140
{
141141
valid: false,
142142
absoluteKeywordLocation: "https://example.com/polygon#/$defs/point/required",
@@ -150,7 +150,10 @@ describe("Error Output Normalization", () => {
150150
]);
151151
});
152152

153-
test("checking for the detailed output format", () => {
153+
test("checking for the detailed output format", async () => {
154+
const schema = {
155+
$id: "https://example.com/polygon"
156+
};
154157
const errorOutput = {
155158
valid: false,
156159
keywordLocation: "#",
@@ -181,12 +184,7 @@ describe("Error Output Normalization", () => {
181184
]
182185
};
183186

184-
expect(normalizeOutputFormat(errorOutput)).to.eql([
185-
{
186-
valid: false,
187-
absoluteKeywordLocation: "https://example.com/polygon#/$defs/point",
188-
instanceLocation: "#/1"
189-
},
187+
expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([
190188
{
191189
valid: false,
192190
absoluteKeywordLocation: "https://example.com/polygon#/$defs/point/required",
@@ -200,7 +198,10 @@ describe("Error Output Normalization", () => {
200198
]);
201199
});
202200

203-
test("checking for the verbose output format", () => {
201+
test("checking for the verbose output format", async () => {
202+
const schema = {
203+
$id: "https://example.com/polygon"
204+
};
204205
const errorOutput = {
205206
valid: false,
206207
keywordLocation: "#",
@@ -232,7 +233,7 @@ describe("Error Output Normalization", () => {
232233
]
233234
};
234235

235-
expect(normalizeOutputFormat(errorOutput)).to.eql([
236+
expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([
236237
{
237238
valid: false,
238239
absoluteKeywordLocation: "https://example.com/schema#/additionalProperties",
@@ -246,7 +247,10 @@ describe("Error Output Normalization", () => {
246247
]);
247248
});
248249

249-
test("when error output doesnot contain any of these three keyword (valid, absoluteKeywordLocation, instanceLocation)", () => {
250+
test("when error output doesnot contain any of these three keyword (valid, absoluteKeywordLocation, instanceLocation)", async () => {
251+
const schema = {
252+
$id: "https://example.com/polygon"
253+
};
250254
const errorOutput = {
251255
valid: false,
252256
errors: [
@@ -256,6 +260,75 @@ describe("Error Output Normalization", () => {
256260
}
257261
]
258262
};
259-
expect(() => normalizeOutputFormat(/** @type any */ (errorOutput))).to.throw("error Output must follow Draft 2019-09");
263+
await expect(async () => normalizeOutputFormat(/** @type any */(errorOutput), schema)).to.rejects.toThrow("error Output must follow Draft 2019-09");
264+
});
265+
266+
test("correctly resolves keywordLocation through $ref in $defs", async () => {
267+
/** @type SchemaObject */
268+
const schema = {
269+
$id: "https://example.com/main",
270+
$schema: "https://json-schema.org/draft/2020-12/schema",
271+
properties: {
272+
foo: { $ref: "#/$defs/lengthDefinition" }
273+
},
274+
$defs: {
275+
lengthDefinition: {
276+
minLength: 3
277+
}
278+
}
279+
};
280+
const instance = { foo: "aa" };
281+
/** @type OutputFormat */
282+
const output = {
283+
valid: false,
284+
errors: [
285+
{
286+
keywordLocation: "/properties/foo/$ref/minLength",
287+
instanceLocation: "#"
288+
}
289+
]
290+
};
291+
292+
const result = await betterJsonSchemaErrors(instance, schema, output);
293+
expect(result.errors).to.eql([
294+
{
295+
schemaLocation: "https://example.com/main#/$defs/lengthDefinition/minLength",
296+
instanceLocation: "#",
297+
message: "The instance should be at least 3 characters"
298+
}
299+
]);
300+
});
301+
302+
test("removes schemaLocation nodes from the error output", async () => {
303+
const schema = {
304+
$id: "https://example.com/polygon"
305+
};
306+
const errorOutput = {
307+
valid: false,
308+
errors: [
309+
{
310+
valid: false,
311+
keywordLocation: "#/items/$ref",
312+
absoluteKeywordLocation: "https://example.com/polygon#/$defs/point",
313+
instanceLocation: "#/1",
314+
error: "A subschema had errors."
315+
},
316+
{
317+
valid: false,
318+
keywordLocation: "#/items/$ref/required",
319+
absoluteKeywordLocation: "https://example.com/polygon#/$defs/point/required",
320+
instanceLocation: "#/1",
321+
error: "Required property 'y' not found."
322+
}
323+
]
324+
};
325+
326+
expect(await normalizeOutputFormat(errorOutput, schema)).to.eql([
327+
{
328+
valid: false,
329+
absoluteKeywordLocation: "https://example.com/polygon#/$defs/point/required",
330+
instanceLocation: "#/1"
331+
}
332+
]);
260333
});
261334
});

0 commit comments

Comments
 (0)