Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
export const betterJsonSchemaErrors: (
instance: Json,
schema: SchemaObject,
errorOutput: OutputFormat
errorOutput: OutputFormat,
options?: BetterJsonSchemaErrorsOptions
) => Promise<BetterJsonSchemaErrors>;

export type BetterJsonSchemaErrors = {
errors: ErrorObject[];
};

export type BetterJsonSchemaErrorsOptions = {
schemaUri?: string;
};

export type ErrorObject = {
schemaLocation: string;
instanceLocation: string;
Expand All @@ -31,6 +35,7 @@ export type OutputFormat = {

export type OutputUnit = {
valid?: boolean;
keyword?: string;
absoluteKeywordLocation?: string;
keywordLocation?: string;
instanceLocation: string;
Expand All @@ -40,6 +45,7 @@ export type OutputUnit = {

export type NormalizedError = {
valid: false;
keyword: string;
absoluteKeywordLocation: string;
instanceLocation: string;
};
55 changes: 50 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,66 @@
import { normalizeOutputFormat } from "./normalizeOutputFormat/normalizeOutput.js";
import * as Schema from "@hyperjump/browser";
import { getSchema } from "@hyperjump/json-schema/experimental";

/**
* @import {betterJsonSchemaErrors} from "./index.d.ts"
* @import { Browser } from "@hyperjump/browser";
* @import { SchemaDocument } from "@hyperjump/json-schema/experimental";
* @import { Json } from "@hyperjump/json-pointer";
* @import {betterJsonSchemaErrors, OutputUnit } from "./index.d.ts"
*/

/** @type betterJsonSchemaErrors */
export async function betterJsonSchemaErrors(instance, schema, errorOutput) {
const normalizedErrors = await normalizeOutputFormat(errorOutput, schema);

export async function betterJsonSchemaErrors(instance, errorOutput, options = {}) {
const normalizedErrors = await normalizeOutputFormat(errorOutput, options.schemaUri);
const errors = [];
for (const error of normalizedErrors) {
if (skip.has(error.keyword)) {
continue;
}

/** @type Browser<SchemaDocument> */
const schema = await getSchema(error.absoluteKeywordLocation);
errors.push({
message: "The instance should be at least 3 characters",
message: getErrorMessage(error, schema, instance),
instanceLocation: error.instanceLocation,
schemaLocation: error.absoluteKeywordLocation
});
}

return { errors };
}

/** @type (outputUnit: OutputUnit, schema: Browser<SchemaDocument>, instance: Json) => string */
const getErrorMessage = (outputUnit, schema) => {
if (outputUnit.keyword === "https://json-schema.org/keyword/minLength") {
return `The instance should be at least ${Schema.value(schema)} characters`;
}

throw Error("TODO: Error message not implemented");
// if (outputUnit.keyword === "https://json-schema.org/keyword/required") {
// const schemaDocument = await Schema.get(outputUnit.absoluteKeywordLocation);
// const required = new Set(Schema.value(schemaDocument));
// const object = Instance.get(outputUnit.instanceLocation, instance);
// for (const propertyName of Instance.keys(object)) {
// required.delete(propertyName);
// }

// return `"${outputUnit.instanceLocation}" is missing required property(s): ${[...required]}. Schema location: ${outputUnit.absoluteKeywordLocation}`;
// } else {
// // Default message
// return `"${outputUnit.instanceLocation}" fails schema constraint ${outputUnit.absoluteKeywordLocation}`;
// }
};

// These are probably not very useful for human readable messaging, so we'll skip them.
const skip = new Set([
"https://json-schema.org/evaluation/validate",
"https://json-schema.org/keyword/ref",
"https://json-schema.org/keyword/properties",
"https://json-schema.org/keyword/patternProperties",
"https://json-schema.org/keyword/items",
"https://json-schema.org/keyword/prefixItems",
"https://json-schema.org/keyword/if",
"https://json-schema.org/keyword/then",
"https://json-schema.org/keyword/else"
]);
34 changes: 34 additions & 0 deletions src/keywordErrorMessage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, test, expect } from "vitest";
import { betterJsonSchemaErrors } from "./index.js";
import { registerSchema } from "@hyperjump/json-schema/draft-2020-12";

describe("Error messages", () => {
test("minLength", async () => {
registerSchema({
$id: "https://example.com/main",
$schema: "https://json-schema.org/draft/2020-12/schema",
minLength: 3
});

const instance = "aa";

/** @type OutputFormat */
const output = {
valid: false,
errors: [
{
absoluteKeywordLocation: "https://example.com/main#/minLength",
instanceLocation: "#"
}
]
};

const result = await betterJsonSchemaErrors(instance, output);
expect(result.errors).to.eql([{
schemaLocation: "https://example.com/main#/minLength",
instanceLocation: "#",
message: "The instance should be at least 3 characters"
}
]);
});
});
32 changes: 12 additions & 20 deletions src/normalizeOutputFormat/normalizeOutput.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as Browser from "@hyperjump/browser";
import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12";
import { getSchema } from "@hyperjump/json-schema/experimental";
import { getSchema, getKeywordId } from "@hyperjump/json-schema/experimental";
import { pointerSegments } from "@hyperjump/json-pointer";
import { randomUUID } from "crypto";

/**
* @import { OutputFormat, OutputUnit, NormalizedError, SchemaObject} from "../index.d.ts";
Expand All @@ -12,10 +10,10 @@ import { randomUUID } from "crypto";

/**
* @param {OutputFormat} errorOutput
* @param {SchemaObject} schema
* @param {string} [schemaUri]
* @returns {Promise<NormalizedError[]>}
*/
export async function normalizeOutputFormat(errorOutput, schema) {
export async function normalizeOutputFormat(errorOutput, schemaUri) {
/** @type {NormalizedError[]} */
const output = [];

Expand All @@ -39,7 +37,7 @@ export async function normalizeOutputFormat(errorOutput, schema) {
}

const absoluteKeywordLocation = error.absoluteKeywordLocation
?? await toAbsoluteKeywordLocation(schema, /** @type string */ (error.keywordLocation));
?? await toAbsoluteKeywordLocation(/** @type string */ (schemaUri), /** @type string */ (error.keywordLocation));

const fragment = absoluteKeywordLocation.split("#")[1];
const lastSegment = fragment.split("/").filter(Boolean).pop();
Expand All @@ -48,6 +46,7 @@ export async function normalizeOutputFormat(errorOutput, schema) {
if (lastSegment && keywords.has(lastSegment)) {
output.push({
valid: false,
keyword: error.keyword ?? getKeywordId(lastSegment, "https://json-schema.org/draft/2020-12/schema"),
absoluteKeywordLocation,
instanceLocation: normalizeInstanceLocation(error.instanceLocation)
});
Expand Down Expand Up @@ -78,22 +77,15 @@ function normalizeInstanceLocation(location) {

/**
* Convert keywordLocation to absoluteKeywordLocation
* @param {SchemaObject} schema
* @param {string} uri
* @param {string} keywordLocation
* @returns {Promise<string>}
*/
export async function toAbsoluteKeywordLocation(schema, keywordLocation) {
const uri = `urn:uuid:${randomUUID()}`;
try {
registerSchema(schema, uri);

let browser = await getSchema(uri);
for (const segment of pointerSegments(keywordLocation)) {
browser = /** @type BrowserType<SchemaDocument> */ (await Browser.step(segment, browser));
}

return `${browser.document.baseUri}#${browser.cursor}`;
} finally {
unregisterSchema(uri);
export async function toAbsoluteKeywordLocation(uri, keywordLocation) {
let browser = await getSchema(uri);
for (const segment of pointerSegments(keywordLocation)) {
browser = /** @type BrowserType<SchemaDocument> */ (await Browser.step(segment, browser));
}

return `${browser.document.baseUri}#${browser.cursor}`;
}
Loading