Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
719 changes: 378 additions & 341 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"vitest": "*"
},
"dependencies": {
"@hyperjump/json-schema": "^1.14.1"
"@hyperjump/browser": "^1.3.1",
"@hyperjump/json-schema": "^1.16.0"
}
}
46 changes: 45 additions & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,45 @@
export const hello: string;
export const betterJsonSchemaErrors: (
instance: Json,
schema: SchemaObject,
errorOutput: OutputFormat
) => Promise<BetterJsonSchemaErrors>;

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

export type ErrorObject = {
schemaLocation: string;
instanceLocation: string;
message: string;
};

export type Json = string | number | boolean | null | JsonObject | Json[];
export type JsonObject = {
[property: string]: Json;
};

export type SchemaFragment = string | number | boolean | null | SchemaObject | SchemaFragment[];
export type SchemaObject = {
[keyword: string]: SchemaFragment;
};

export type OutputFormat = {
valid: boolean;
errors: OutputUnit[];
};

export type OutputUnit = {
valid?: boolean;
absoluteKeywordLocation?: string;
keywordLocation?: string;
instanceLocation: string;
error?: string;
errors?: OutputUnit[];
};

export type NormalizedError = {
valid: false;
absoluteKeywordLocation: string;
instanceLocation: string;
};
21 changes: 18 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { normalizeOutputFormat } from "./normalizeOutputFormat/normalizeOutput.js";

/**
* @import * as API from "./index.d.ts"
* @import {betterJsonSchemaErrors} from "./index.d.ts"
*/

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

const errors = [];
for (const error of normalizedErrors) {
errors.push({
message: "The instance should be at least 3 characters",
instanceLocation: error.instanceLocation,
schemaLocation: error.absoluteKeywordLocation
});
}

return { errors };
}
8 changes: 0 additions & 8 deletions src/index.test.js

This file was deleted.

6 changes: 6 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { describe, test } from "vitest";

describe("Better JSON Schema Errors", () => {
test("greeting", () => {
});
});
99 changes: 99 additions & 0 deletions src/normalizeOutputFormat/normalizeOutput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as Browser from "@hyperjump/browser";
import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12";
import { getSchema } from "@hyperjump/json-schema/experimental";
import { pointerSegments } from "@hyperjump/json-pointer";
import { randomUUID } from "crypto";

/**
* @import { OutputFormat, OutputUnit, NormalizedError, SchemaObject} from "../index.d.ts";
* @import { SchemaDocument } from "@hyperjump/json-schema/experimental";
* @import { Browser as BrowserType } from "@hyperjump/browser";
*/

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

if (!errorOutput || errorOutput.valid !== false) {
throw new Error("error Output must follow Draft 2019-09");
}

const keywords = new Set([
"type", "minLength", "maxLength", "minimum", "maximum", "format", "pattern",
"enum", "const", "required", "items", "properties", "allOf", "anyOf", "oneOf",
"not", "contains", "uniqueItems", "additionalProperties", "minItems", "maxItems",
"minProperties", "maxProperties", "dependentRequired", "dependencies"
]);

/** @type {(errorOutput: OutputUnit) => Promise<void>} */
async function collectErrors(error) {
if (error.valid) return;

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

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

const fragment = absoluteKeywordLocation.split("#")[1];
const lastSegment = fragment.split("/").filter(Boolean).pop();

// make a check here to remove the schemaLocation.
if (lastSegment && keywords.has(lastSegment)) {
output.push({
valid: false,
absoluteKeywordLocation,
instanceLocation: normalizeInstanceLocation(error.instanceLocation)
});
}

if (error.errors) {
for (const nestedError of error.errors) {
await collectErrors(nestedError); // Recursive
}
}
}

if (!errorOutput.errors) {
throw new Error("error Output must follow Draft 2019-09");
}

for (const err of errorOutput.errors) {
await collectErrors(err);
}

return output;
}

/** @type {(location: string) => string} */
function normalizeInstanceLocation(location) {
return location.startsWith("/") || location === "" ? `#${location}` : location;
}

/**
* Convert keywordLocation to absoluteKeywordLocation
* @param {SchemaObject} schema
* @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);
}
}
Loading