Skip to content

Done normalizing the OutputFormat #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 20, 2025
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