From e44820e26d16217134558d514c1fb8dd78fbdd8e Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Fri, 3 Apr 2026 21:47:29 -0700 Subject: [PATCH 1/3] Fix some remote categorization for easier dialect-specific loading --- remotes/draft2019-09/ignore-dependentRequired.json | 7 +++++++ remotes/draft2019-09/prefixItems.json | 7 +++++++ remotes/draft2020-12/ignore-prefixItems.json | 7 +++++++ remotes/draft7/dependentRequired.json | 7 +++++++ tests/draft2019-09/optional/cross-draft.json | 4 ++-- tests/draft2020-12/optional/cross-draft.json | 2 +- tests/draft7/optional/cross-draft.json | 2 +- tests/v1/ref.json | 2 +- 8 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 remotes/draft2019-09/ignore-dependentRequired.json create mode 100644 remotes/draft2019-09/prefixItems.json create mode 100644 remotes/draft2020-12/ignore-prefixItems.json create mode 100644 remotes/draft7/dependentRequired.json diff --git a/remotes/draft2019-09/ignore-dependentRequired.json b/remotes/draft2019-09/ignore-dependentRequired.json new file mode 100644 index 00000000..f2eb5048 --- /dev/null +++ b/remotes/draft2019-09/ignore-dependentRequired.json @@ -0,0 +1,7 @@ +{ + "$id": "http://localhost:1234/draft2019-09/integer.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "dependentRequired": { + "foo": ["bar"] + } +} diff --git a/remotes/draft2019-09/prefixItems.json b/remotes/draft2019-09/prefixItems.json new file mode 100644 index 00000000..acd8293c --- /dev/null +++ b/remotes/draft2019-09/prefixItems.json @@ -0,0 +1,7 @@ +{ + "$id": "http://localhost:1234/draft2020-12/prefixItems.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + {"type": "string"} + ] +} diff --git a/remotes/draft2020-12/ignore-prefixItems.json b/remotes/draft2020-12/ignore-prefixItems.json new file mode 100644 index 00000000..0ef44663 --- /dev/null +++ b/remotes/draft2020-12/ignore-prefixItems.json @@ -0,0 +1,7 @@ +{ + "$id": "http://localhost:1234/draft2020-12/ignore-prefixItems.json", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "prefixItems": [ + {"type": "string"} + ] +} diff --git a/remotes/draft7/dependentRequired.json b/remotes/draft7/dependentRequired.json new file mode 100644 index 00000000..f16a3450 --- /dev/null +++ b/remotes/draft7/dependentRequired.json @@ -0,0 +1,7 @@ +{ + "$id": "http://localhost:1234/draft7/dependentRequired.json", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "dependentRequired": { + "foo": ["bar"] + } +} diff --git a/tests/draft2019-09/optional/cross-draft.json b/tests/draft2019-09/optional/cross-draft.json index efd3f87d..a96ae506 100644 --- a/tests/draft2019-09/optional/cross-draft.json +++ b/tests/draft2019-09/optional/cross-draft.json @@ -4,7 +4,7 @@ "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "type": "array", - "$ref": "http://localhost:1234/draft2020-12/prefixItems.json" + "$ref": "http://localhost:1234/draft2019-09/prefixItems.json" }, "tests": [ { @@ -26,7 +26,7 @@ "type": "object", "allOf": [ { "properties": { "foo": true } }, - { "$ref": "http://localhost:1234/draft7/ignore-dependentRequired.json" } + { "$ref": "http://localhost:1234/draft2019-09/ignore-dependentRequired.json" } ] }, "tests": [ diff --git a/tests/draft2020-12/optional/cross-draft.json b/tests/draft2020-12/optional/cross-draft.json index 5113bd64..c21b218c 100644 --- a/tests/draft2020-12/optional/cross-draft.json +++ b/tests/draft2020-12/optional/cross-draft.json @@ -4,7 +4,7 @@ "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "array", - "$ref": "http://localhost:1234/draft2019-09/ignore-prefixItems.json" + "$ref": "http://localhost:1234/draft2020-12/ignore-prefixItems.json" }, "tests": [ { diff --git a/tests/draft7/optional/cross-draft.json b/tests/draft7/optional/cross-draft.json index 8ff53736..026919fc 100644 --- a/tests/draft7/optional/cross-draft.json +++ b/tests/draft7/optional/cross-draft.json @@ -5,7 +5,7 @@ "type": "object", "allOf": [ { "properties": { "foo": true } }, - { "$ref": "http://localhost:1234/draft2019-09/dependentRequired.json" } + { "$ref": "http://localhost:1234/draft7/dependentRequired.json" } ] }, "tests": [ diff --git a/tests/v1/ref.json b/tests/v1/ref.json index dde3e5b1..8876195d 100644 --- a/tests/v1/ref.json +++ b/tests/v1/ref.json @@ -185,7 +185,7 @@ "description": "remote ref, containing refs itself", "schema": { "$schema": "https://json-schema.org/v1", - "$ref": "http://localhost:1234/v1/ref-and-defs" + "$ref": "http://localhost:1234/v1/ref-and-defs.json" }, "tests": [ { From 9bd13e9dfc1bf4cae793fde48a9a63833cdab3a1 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Fri, 3 Apr 2026 22:09:21 -0700 Subject: [PATCH 2/3] Add a script and action to generate ids for tests Co-authored-by: Anirudh Jindal --- .github/workflows/apply-test-ids.yml | 60 ++++++ .gitignore | 1 + package-lock.json | 182 ++++++++++++++++ package.json | 12 +- scripts/generate-ids-for.js | 9 + scripts/utils/test-ids.js | 305 +++++++++++++++++++++++++++ test-schema.json | 4 + 7 files changed, 572 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/apply-test-ids.yml create mode 100644 package-lock.json create mode 100644 scripts/generate-ids-for.js create mode 100644 scripts/utils/test-ids.js diff --git a/.github/workflows/apply-test-ids.yml b/.github/workflows/apply-test-ids.yml new file mode 100644 index 00000000..0dda3cad --- /dev/null +++ b/.github/workflows/apply-test-ids.yml @@ -0,0 +1,60 @@ +name: Test IDs + +on: + pull_request_target: + types: [opened, synchronize] + paths: + - 'tests/**/*.json' + +permissions: + contents: write + +jobs: + generate: + runs-on: ubuntu-latest + + steps: + - name: Detect changed files + uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + other: + - '!tests/**/*.json' + + - name: Block mixed changes + if: steps.changes.outputs.other == 'true' + run: | + echo "Tests and other files were changed together." + echo "Please submit test JSON changes separately." + exit 1 + + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: latest + + - name: Install dependencies + run: npm ci + + - name: Generate test IDs + run: | + node scripts/generate-ids-for.js draft2020-12 + node scripts/generate-ids-for.js draft2019-09 + node scripts/generate-ids-for.js draft7 + node scripts/generate-ids-for.js draft6 + node scripts/generate-ids-for.js draft4 + node scripts/generate-ids-for.js v1 + + - name: Commit and push + run: | + git config user.name "test-id-bot" + git config user.email "test-id-bot@users.noreply.github.com" + git add tests/ + git diff --cached --quiet || git commit -m "chore: auto-add missing test IDs" + git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.event.pull_request.head.repo.full_name }}.git HEAD:refs/heads/${{ github.event.pull_request.head.ref }} diff --git a/.gitignore b/.gitignore index 68bc17f9..6953a1ef 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +node_modules/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..022321b7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,182 @@ +{ + "name": "json-schema-test-suite", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "json-schema-test-suite", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@hyperjump/browser": "^1.3.1", + "@hyperjump/json-pointer": "^1.1.1", + "@hyperjump/json-schema": "^1.17.4", + "@hyperjump/pact": "^1.4.0", + "@hyperjump/uri": "^1.3.2", + "json-stringify-deterministic": "^1.0.12", + "jsonc-parser": "^3.3.1" + } + }, + "node_modules/@hyperjump/browser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@hyperjump/browser/-/browser-1.3.1.tgz", + "integrity": "sha512-Le5XZUjnVqVjkgLYv6yyWgALat/0HpB1XaCPuCZ+GCFki9NvXloSZITIJ0H+wRW7mb9At1SxvohKBbNQbrr/cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hyperjump/json-pointer": "^1.1.0", + "@hyperjump/uri": "^1.2.0", + "content-type": "^1.0.5", + "just-curry-it": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + } + }, + "node_modules/@hyperjump/json-pointer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@hyperjump/json-pointer/-/json-pointer-1.1.2.tgz", + "integrity": "sha512-zPNgu1zdhtjQHFNLGzvEsLDsLOEvhRj6u6ktIQmlz7YPESv5uF8SnAe3Dq0oL6gZ6OGWSLq2n7pphRNF6Hpg6w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + } + }, + "node_modules/@hyperjump/json-schema": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.17.4.tgz", + "integrity": "sha512-5J1onqwejDS4Uytzu+qKh09szi3PIinkSjsjpXFtXrVU+Jkzii+sgKcKnFLaAhF7f0gUfPqhB2GtLdRdP9pIhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hyperjump/json-pointer": "^1.1.0", + "@hyperjump/json-schema-formats": "^1.0.0", + "@hyperjump/pact": "^1.2.0", + "@hyperjump/uri": "^1.2.0", + "content-type": "^1.0.4", + "json-stringify-deterministic": "^1.0.12", + "just-curry-it": "^5.3.0", + "uuid": "^9.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + }, + "peerDependencies": { + "@hyperjump/browser": "^1.1.0" + } + }, + "node_modules/@hyperjump/json-schema-formats": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@hyperjump/json-schema-formats/-/json-schema-formats-1.0.1.tgz", + "integrity": "sha512-qvcIxysnMfcPxyPSFFzzo28o2BN1CNT5b0tQXNUP0kaFpvptQNDg8SCLvlnMg2sYxuiuqna8+azGBaBthiskAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hyperjump/uri": "^1.3.2", + "idn-hostname": "^15.1.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/hyperjump-io" + } + }, + "node_modules/@hyperjump/pact": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@hyperjump/pact/-/pact-1.4.0.tgz", + "integrity": "sha512-01Q7VY6BcAkp9W31Fv+ciiZycxZHGlR2N6ba9BifgyclHYHdbaZgITo0U6QMhYRlem4k8pf8J31/tApxvqAz8A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + } + }, + "node_modules/@hyperjump/uri": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@hyperjump/uri/-/uri-1.3.3.tgz", + "integrity": "sha512-rUqeUdL2aW7lzvSnCL6yUetXYzqxhsBEw9Z7Y1bEhgiRzcfO3kjY0UdD6c4H/bzxe0fXIjYuocjWQzinio8JTQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jdesrosiers" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/idn-hostname": { + "version": "15.1.8", + "resolved": "https://registry.npmjs.org/idn-hostname/-/idn-hostname-15.1.8.tgz", + "integrity": "sha512-MmLwddtSVyMtzYxx+xs2IFEbfyg/facubL/mEaAoJX/XIfjt1ly5QhPByihf4yrxZYbkQfRZVEnBgISv/e2ZWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + } + }, + "node_modules/json-stringify-deterministic": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/json-stringify-deterministic/-/json-stringify-deterministic-1.0.12.tgz", + "integrity": "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/just-curry-it": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-5.3.0.tgz", + "integrity": "sha512-silMIRiFjUWlfaDhkgSzpuAyQ6EX/o09Eu8ZBfmFwQMbax7+LQzeIU2CBrICT6Ne4l86ITCGvUCBpCubWYy0Yw==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + } + } +} diff --git a/package.json b/package.json index 75da9e29..5c0c5167 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "json-schema-test-suite", "version": "0.1.0", + "type": "module", "description": "A language agnostic test suite for the JSON Schema specifications", "repository": "github:json-schema-org/JSON-Schema-Test-Suite", "keywords": [ @@ -8,5 +9,14 @@ "tests" ], "author": "http://json-schema.org", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "@hyperjump/browser": "^1.3.1", + "@hyperjump/json-pointer": "^1.1.1", + "@hyperjump/json-schema": "^1.17.4", + "@hyperjump/pact": "^1.4.0", + "@hyperjump/uri": "^1.3.2", + "json-stringify-deterministic": "^1.0.12", + "jsonc-parser": "^3.3.1" + } } diff --git a/scripts/generate-ids-for.js b/scripts/generate-ids-for.js new file mode 100644 index 00000000..9ce59b33 --- /dev/null +++ b/scripts/generate-ids-for.js @@ -0,0 +1,9 @@ +import { generateIdsFor } from "./utils/test-ids.js"; + +const version = process.argv[2]; +if (!version) { + console.error("Usage: node scripts/generate-ids-for.js "); + process.exit(1); +} + +await generateIdsFor(version); diff --git a/scripts/utils/test-ids.js b/scripts/utils/test-ids.js new file mode 100644 index 00000000..ca5beec9 --- /dev/null +++ b/scripts/utils/test-ids.js @@ -0,0 +1,305 @@ +import * as crypto from "node:crypto"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import jsonStringify from "json-stringify-deterministic"; +import { applyEdits, modify } from "jsonc-parser"; + +import * as Pact from "@hyperjump/pact"; +import * as JsonPointer from "@hyperjump/json-pointer"; +import * as Schema from "@hyperjump/browser"; +import { registerSchema, unregisterSchema } from "@hyperjump/json-schema"; +import { getSchema, getKeywordId, getKeywordName } from "@hyperjump/json-schema/experimental"; +import "@hyperjump/json-schema/draft-2020-12"; +import "@hyperjump/json-schema/draft-2019-09"; +import "@hyperjump/json-schema/draft-07"; +import "@hyperjump/json-schema/draft-06"; +import "@hyperjump/json-schema/draft-04"; + +const DIALECT_MAP = { + "v1": "https://json-schema.org/v1", + "draft2020-12": "https://json-schema.org/draft/2020-12/schema", + "draft2019-09": "https://json-schema.org/draft/2019-09/schema", + "draft7": "http://json-schema.org/draft-07/schema", + "draft6": "http://json-schema.org/draft-06/schema", + "draft4": "http://json-schema.org/draft-04/schema", + "draft3": "http://json-schema.org/draft-03/schema" +}; + +export const generateIdsFor = async (version) => { + const dialectUri = DIALECT_MAP[version]; + + const registeredSchemas = await loadRemotes(version); + + for (const entry of await fs.readdir(`./tests/${version}`, { recursive: true, withFileTypes: true })) { + if (!entry.isFile() || path.extname(entry.name) !== ".json") { + continue; + } + + const edits = []; + const filePath = path.resolve(entry.parentPath, entry.name); + const json = await fs.readFile(filePath, "utf8") + const suite = JSON.parse(json); + + for (let testCaseIndex = 0; testCaseIndex < suite.length; testCaseIndex++) { + const testCase = suite[testCaseIndex]; + + try { + const normalizedSchema = await normalizeSchema(testCase.schema, dialectUri); + + for (let testIndex = 0; testIndex < testCase.tests.length; testIndex++) { + const test = testCase.tests[testIndex]; + const id = generateTestId(normalizedSchema, test.data, test.valid); + + edits.push(...modify(json, [testCaseIndex, "tests", testIndex, "id"], id, { + formattingOptions: { + insertSpaces: true, + tabSize: 4 + } + })); + } + } catch (error) { + console.log(`Failed to generate an ID for ${version} ${entry.name}: ${testCase.description}`); + // console.log(error); + } + } + + if (edits.length > 0) { + const updatedJson = applyEdits(json, edits); + await fs.writeFile(filePath, updatedJson); + } + } + + for (const remoteUri of registeredSchemas) { + unregisterSchema(remoteUri); + } +}; + +export const loadRemotes = async (version, filePath = `./remotes`, url = "") => { + const registeredSchemas = []; + + for (const entry of await fs.readdir(filePath, { withFileTypes: true })) { + if (entry.isFile() && path.extname(entry.name) === ".json") { + const remote = JSON.parse(await fs.readFile(`${filePath}/${entry.name}`, "utf8")); + const schemaUri = `http://localhost:1234${url}/${entry.name}`; + registerSchema(remote, schemaUri, DIALECT_MAP[version]); + registeredSchemas.push(schemaUri); + } else if (entry.isDirectory() && entry.name === version || !(entry.name in DIALECT_MAP)) { + registeredSchemas.push(...await loadRemotes(version, `${filePath}/${entry.name}`, `${url}/${entry.name}`)); + } + } + + return registeredSchemas; +}; + +export const generateTestId = (normalizedSchema, testData, testValid) => { + return crypto + .createHash("md5") + .update(jsonStringify(normalizedSchema) + jsonStringify(testData) + testValid) + .digest("hex"); +}; + +export const normalizeSchema = async (rawSchema, dialectUri) => { + const schemaUri = "https://test-suite.json-schema.org/main"; + + try { + const safeSchema = sanitizeTopLevelId(rawSchema, dialectUri); + registerSchema(safeSchema, schemaUri, dialectUri); + + const schema = await getSchema(schemaUri); + const ast = { metaData: {} }; + await compile(schema, ast); + return ast; + } finally { + unregisterSchema(schemaUri); + } +}; + +const sanitizeTopLevelId = (schema, dialectUri) => { + if (typeof schema !== "object") { + return schema; + } + + const idToken = getKeywordName(dialectUri, "https://json-schema.org/keyword/id") + ?? getKeywordName(dialectUri, "https://json-schema.org/keyword/draft-04/id"); + if (idToken in schema) { + schema[idToken] = schema[idToken].replace(/^file:/, "x-file:"); + } + + return schema; +}; + +const compile = async (schema, ast) => { + if (!(schema.document.baseUri in ast.metaData)) { + ast.metaData[schema.document.baseUri] = { + anchors: schema.document.anchors, + dynamicAnchors: schema.document.dynamicAnchors + }; + } + + const url = canonicalUri(schema); + if (!(url in ast)) { + const schemaValue = Schema.value(schema); + if (!["object", "boolean"].includes(typeof schemaValue)) { + throw Error(`No schema found at '${url}'`); + } + + if (typeof schemaValue === "boolean") { + ast[url] = schemaValue; + } else { + ast[url] = []; + for await (const [keyword, keywordSchema] of Schema.entries(schema)) { + const keywordUri = getKeywordId(keyword, schema.document.dialectId); + if (!keywordUri || keywordUri === "https://json-schema.org/keyword/comment") { + continue; + } + + ast[url].push({ + keyword: keywordUri, + location: JsonPointer.append(keyword, canonicalUri(schema)), + value: await getKeywordHandler(keywordUri)(keywordSchema, ast, schema) + }); + } + } + } + + return url; +}; + +const canonicalUri = (schema) => `${schema.document.baseUri}#${encodeURI(schema.cursor)}`; + +const getKeywordHandler = (keywordUri) => { + if (keywordUri in keywordHandlers) { + return keywordHandlers[keywordUri]; + } else if (keywordUri.startsWith("https://json-schema.org/keyword/unknown#")) { + return keywordHandlers["https://json-schema.org/keyword/unknown"]; + } else { + throw Error(`Missing handler for keyword: ${keywordUri}`); + } +}; + +const simpleValue = (keyword) => Schema.value(keyword); + +const simpleApplicator = (keyword, ast) => compile(keyword, ast); + +const objectApplicator = (keyword, ast) => { + return Pact.pipe( + Schema.entries(keyword), + Pact.asyncMap(async ([propertyName, subSchema]) => [propertyName, await compile(subSchema, ast)]), + Pact.asyncCollectObject + ); +}; + +const arrayApplicator = (keyword, ast) => { + return Pact.pipe( + Schema.iter(keyword), + Pact.asyncMap(async (subSchema) => await compile(subSchema, ast)), + Pact.asyncCollectArray + ); +}; + +const keywordHandlers = { + "https://json-schema.org/keyword/additionalProperties": simpleApplicator, + "https://json-schema.org/keyword/allOf": arrayApplicator, + "https://json-schema.org/keyword/anyOf": arrayApplicator, + "https://json-schema.org/keyword/const": simpleValue, + "https://json-schema.org/keyword/contains": simpleApplicator, + "https://json-schema.org/keyword/contentEncoding": simpleValue, + "https://json-schema.org/keyword/contentMediaType": simpleValue, + "https://json-schema.org/keyword/contentSchema": simpleApplicator, + "https://json-schema.org/keyword/default": simpleValue, + "https://json-schema.org/keyword/definitions": objectApplicator, + "https://json-schema.org/keyword/dependentRequired": simpleValue, + "https://json-schema.org/keyword/dependentSchemas": objectApplicator, + "https://json-schema.org/keyword/deprecated": simpleValue, + "https://json-schema.org/keyword/description": simpleValue, + "https://json-schema.org/keyword/dynamicRef": simpleValue, + "https://json-schema.org/keyword/else": simpleApplicator, + "https://json-schema.org/keyword/enum": simpleValue, + "https://json-schema.org/keyword/examples": simpleValue, + "https://json-schema.org/keyword/exclusiveMaximum": simpleValue, + "https://json-schema.org/keyword/exclusiveMinimum": simpleValue, + "https://json-schema.org/keyword/format": simpleValue, + "https://json-schema.org/keyword/if": simpleApplicator, + "https://json-schema.org/keyword/items": simpleApplicator, + "https://json-schema.org/keyword/maxContains": simpleValue, + "https://json-schema.org/keyword/maxItems": simpleValue, + "https://json-schema.org/keyword/maxLength": simpleValue, + "https://json-schema.org/keyword/maxProperties": simpleValue, + "https://json-schema.org/keyword/maximum": simpleValue, + "https://json-schema.org/keyword/minContains": simpleValue, + "https://json-schema.org/keyword/minItems": simpleValue, + "https://json-schema.org/keyword/minLength": simpleValue, + "https://json-schema.org/keyword/minProperties": simpleValue, + "https://json-schema.org/keyword/minimum": simpleValue, + "https://json-schema.org/keyword/multipleOf": simpleValue, + "https://json-schema.org/keyword/not": simpleApplicator, + "https://json-schema.org/keyword/oneOf": arrayApplicator, + "https://json-schema.org/keyword/pattern": simpleValue, + "https://json-schema.org/keyword/patternProperties": objectApplicator, + "https://json-schema.org/keyword/prefixItems": arrayApplicator, + "https://json-schema.org/keyword/properties": objectApplicator, + "https://json-schema.org/keyword/propertyDependencies": (keyword, ast) => { + return Pact.pipe( + Schema.entries(keyword), + Pact.asyncMap(async ([propertyName, valueSchemaMap]) => { + return [ + propertyName, + await Pact.pipe( + Schema.entries(valueSchemaMap), + Pact.asyncMap(async ([propertyValue, schema]) => [propertyValue, await compile(schema, ast)]), + Pact.asyncCollectObject + ) + ]; + }), + Pact.asyncCollectObject + ); + }, + "https://json-schema.org/keyword/propertyNames": simpleApplicator, + "https://json-schema.org/keyword/readOnly": simpleValue, + "https://json-schema.org/keyword/ref": compile, + "https://json-schema.org/keyword/required": simpleValue, + "https://json-schema.org/keyword/title": simpleValue, + "https://json-schema.org/keyword/then": simpleApplicator, + "https://json-schema.org/keyword/type": simpleValue, + "https://json-schema.org/keyword/unevaluatedItems": simpleApplicator, + "https://json-schema.org/keyword/unevaluatedProperties": simpleApplicator, + "https://json-schema.org/keyword/uniqueItems": simpleValue, + "https://json-schema.org/keyword/unknown": simpleValue, + "https://json-schema.org/keyword/writeOnly": simpleValue, + + "https://json-schema.org/keyword/draft-2020-12/dynamicRef": simpleValue, + "https://json-schema.org/keyword/draft-2020-12/format": simpleValue, + "https://json-schema.org/keyword/draft-2020-12/format-assertion": simpleValue, + + "https://json-schema.org/keyword/draft-2019-09/formatAssertion": simpleValue, + "https://json-schema.org/keyword/draft-2019-09/format": simpleValue, + + "https://json-schema.org/keyword/draft-07/format": simpleValue, + + "https://json-schema.org/keyword/draft-06/contains": simpleApplicator, + "https://json-schema.org/keyword/draft-06/format": simpleValue, + + "https://json-schema.org/keyword/draft-04/additionalItems": simpleApplicator, + "https://json-schema.org/keyword/draft-04/dependencies": (keyword, ast) => { + return Pact.pipe( + Schema.entries(keyword), + Pact.asyncMap(async ([propertyName, schema]) => { + return [ + propertyName, + Schema.typeOf(schema) === "array" ? Schema.value(schema) : await compile(schema, ast) + ]; + }), + Pact.asyncCollectObject + ); + }, + "https://json-schema.org/keyword/draft-04/exclusiveMaximum": simpleValue, + "https://json-schema.org/keyword/draft-04/exclusiveMinimum": simpleValue, + "https://json-schema.org/keyword/draft-04/format": simpleValue, + "https://json-schema.org/keyword/draft-04/items": (keyword, ast) => { + return Schema.typeOf(keyword) === "array" + ? arrayApplicator(keyword, ast) + : simpleApplicator(keyword, ast); + }, + "https://json-schema.org/keyword/draft-04/maximum": simpleValue, + "https://json-schema.org/keyword/draft-04/minimum": simpleValue +}; diff --git a/test-schema.json b/test-schema.json index 0087c5e3..770a7510 100644 --- a/test-schema.json +++ b/test-schema.json @@ -116,6 +116,10 @@ "valid": { "description": "Whether the validation process of this instance should consider the instance valid or not", "type": "boolean" + }, + "id": { + "description": "Stable identifier for this test", + "type": "string" } }, "additionalProperties": false From cf7eefd4b6350a0059d02357f08cdafb33094277 Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Fri, 3 Apr 2026 22:20:40 -0700 Subject: [PATCH 3/3] Add ids manually for a couple tests that can't be generated --- tests/draft4/refRemote.json | 6 ++++-- tests/draft6/refRemote.json | 6 ++++-- tests/draft7/refRemote.json | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/draft4/refRemote.json b/tests/draft4/refRemote.json index 65e45190..5efff1d6 100644 --- a/tests/draft4/refRemote.json +++ b/tests/draft4/refRemote.json @@ -22,12 +22,14 @@ { "description": "remote fragment valid", "data": 1, - "valid": true + "valid": true, + "id": "79fa52a64be1ec8cb81af9bcf777bf34" }, { "description": "remote fragment invalid", "data": "a", - "valid": false + "valid": false, + "id": "f1d62e8ef0c9769bb84a10c5eeaf96d8" } ] }, diff --git a/tests/draft6/refRemote.json b/tests/draft6/refRemote.json index 49ead6d1..faf38296 100644 --- a/tests/draft6/refRemote.json +++ b/tests/draft6/refRemote.json @@ -124,12 +124,14 @@ { "description": "number is valid", "data": {"list": [1]}, - "valid": true + "valid": true, + "id": "79fa52a64be1ec8cb81af9bcf777bf34" }, { "description": "string is invalid", "data": {"list": ["a"]}, - "valid": false + "valid": false, + "id": "f1d62e8ef0c9769bb84a10c5eeaf96d8" } ] }, diff --git a/tests/draft7/refRemote.json b/tests/draft7/refRemote.json index 450787af..8c7177c1 100644 --- a/tests/draft7/refRemote.json +++ b/tests/draft7/refRemote.json @@ -124,12 +124,14 @@ { "description": "number is valid", "data": {"list": [1]}, - "valid": true + "valid": true, + "id": "79fa52a64be1ec8cb81af9bcf777bf34" }, { "description": "string is invalid", "data": {"list": ["a"]}, - "valid": false + "valid": false, + "id": "f1d62e8ef0c9769bb84a10c5eeaf96d8" } ] },