Skip to content

Commit 44fc7b2

Browse files
committed
Add test IDs to draft2020-12/enum.json using normalized schema hash (POC for #698)
1 parent a247442 commit 44fc7b2

File tree

6 files changed

+954
-337
lines changed

6 files changed

+954
-337
lines changed

package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
{
22
"name": "json-schema-test-suite",
33
"version": "0.1.0",
4+
"type": "module",
45
"description": "A language agnostic test suite for the JSON Schema specifications",
56
"repository": "github:json-schema-org/JSON-Schema-Test-Suite",
67
"keywords": [
78
"json-schema",
89
"tests"
910
],
1011
"author": "http://json-schema.org",
11-
"license": "MIT"
12+
"license": "MIT",
13+
"dependencies": {
14+
"@hyperjump/browser": "^1.3.1",
15+
"@hyperjump/json-pointer": "^1.1.1",
16+
"@hyperjump/json-schema": "^1.17.2",
17+
"@hyperjump/pact": "^1.4.0",
18+
"@hyperjump/uri": "^1.3.2",
19+
"json-stringify-deterministic": "^1.0.12"
20+
}
1221
}

scripts/add-test-ids.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as fs from "node:fs";
2+
import * as crypto from "node:crypto";
3+
import jsonStringify from "json-stringify-deterministic";
4+
import { normalize } from "./normalize.js";
5+
import { loadRemotes } from "./load-remotes.js";
6+
7+
const DIALECT_MAP = {
8+
"https://json-schema.org/draft/2020-12/schema": "https://json-schema.org/draft/2020-12/schema",
9+
"https://json-schema.org/draft/2019-09/schema": "https://json-schema.org/draft/2019-09/schema",
10+
"http://json-schema.org/draft-07/schema#": "http://json-schema.org/draft-07/schema#",
11+
"http://json-schema.org/draft-06/schema#": "http://json-schema.org/draft-06/schema#",
12+
"http://json-schema.org/draft-04/schema#": "http://json-schema.org/draft-04/schema#"
13+
};
14+
15+
function getDialectUri(schema) {
16+
if (schema.$schema && DIALECT_MAP[schema.$schema]) {
17+
return DIALECT_MAP[schema.$schema];
18+
}
19+
return "https://json-schema.org/draft/2020-12/schema";
20+
}
21+
22+
function generateTestId(normalizedSchema, testData, testValid) {
23+
return crypto
24+
.createHash("md5")
25+
.update(jsonStringify(normalizedSchema) + jsonStringify(testData) + testValid)
26+
.digest("hex");
27+
}
28+
29+
async function addIdsToFile(filePath) {
30+
console.log("Reading:", filePath);
31+
const tests = JSON.parse(fs.readFileSync(filePath, "utf8"));
32+
let changed = false;
33+
let added = 0;
34+
35+
if (!Array.isArray(tests)) {
36+
console.log("Expected an array at top level, got:", typeof tests);
37+
return;
38+
}
39+
40+
for (const testCase of tests) {
41+
if (!Array.isArray(testCase.tests)) continue;
42+
43+
const dialectUri = getDialectUri(testCase.schema || {});
44+
const normalizedSchema = await normalize(testCase.schema || true, dialectUri);
45+
46+
for (const test of testCase.tests) {
47+
if (!test.id) {
48+
test.id = generateTestId(normalizedSchema, test.data, test.valid);
49+
changed = true;
50+
added++;
51+
}
52+
}
53+
}
54+
55+
if (changed) {
56+
fs.writeFileSync(filePath, JSON.stringify(tests, null, 2) + "\n");
57+
console.log(`✓ Added ${added} IDs`);
58+
} else {
59+
console.log("✓ All tests already have IDs");
60+
}
61+
}
62+
63+
// Load remotes for all dialects
64+
const remotesPaths = ["./remotes"];
65+
for (const dialectUri of Object.values(DIALECT_MAP)) {
66+
for (const path of remotesPaths) {
67+
if (fs.existsSync(path)) {
68+
loadRemotes(dialectUri, path);
69+
}
70+
}
71+
}
72+
73+
const filePath = process.argv[2] || "tests/draft2020-12/enum.json";
74+
addIdsToFile(filePath).catch(console.error);

scripts/check-test-ids.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import * as fs from "node:fs";
2+
import * as path from "node:path";
3+
import * as crypto from "node:crypto";
4+
import jsonStringify from "json-stringify-deterministic";
5+
import { normalize } from "./normalize.js";
6+
import { loadRemotes } from "./load-remotes.js";
7+
8+
const DIALECT_MAP = {
9+
"https://json-schema.org/draft/2020-12/schema": "https://json-schema.org/draft/2020-12/schema",
10+
"https://json-schema.org/draft/2019-09/schema": "https://json-schema.org/draft/2019-09/schema",
11+
"http://json-schema.org/draft-07/schema#": "http://json-schema.org/draft-07/schema#",
12+
"http://json-schema.org/draft-06/schema#": "http://json-schema.org/draft-06/schema#",
13+
"http://json-schema.org/draft-04/schema#": "http://json-schema.org/draft-04/schema#"
14+
};
15+
16+
function* jsonFiles(dir) {
17+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
18+
const full = path.join(dir, entry.name);
19+
if (entry.isDirectory()) {
20+
yield* jsonFiles(full);
21+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
22+
yield full;
23+
}
24+
}
25+
}
26+
27+
function getDialectUri(schema) {
28+
if (schema.$schema && DIALECT_MAP[schema.$schema]) {
29+
return DIALECT_MAP[schema.$schema];
30+
}
31+
return "https://json-schema.org/draft/2020-12/schema";
32+
}
33+
34+
function generateTestId(normalizedSchema, testData, testValid) {
35+
return crypto
36+
.createHash("md5")
37+
.update(jsonStringify(normalizedSchema) + jsonStringify(testData) + testValid)
38+
.digest("hex");
39+
}
40+
41+
async function checkVersion(dir) {
42+
const missingIdFiles = new Set();
43+
const duplicateIdFiles = new Set();
44+
const mismatchedIdFiles = new Set();
45+
const idMap = new Map();
46+
47+
console.log(`Checking tests in ${dir}...`);
48+
49+
for (const file of jsonFiles(dir)) {
50+
const tests = JSON.parse(fs.readFileSync(file, "utf8"));
51+
52+
for (let i = 0; i < tests.length; i++) {
53+
const testCase = tests[i];
54+
if (!Array.isArray(testCase.tests)) continue;
55+
56+
const dialectUri = getDialectUri(testCase.schema || {});
57+
const normalizedSchema = await normalize(testCase.schema || true, dialectUri);
58+
59+
for (let j = 0; j < testCase.tests.length; j++) {
60+
const test = testCase.tests[j];
61+
62+
if (!test.id) {
63+
missingIdFiles.add(file);
64+
console.log(` ✗ Missing ID: ${file} | ${testCase.description} | ${test.description}`);
65+
continue;
66+
}
67+
68+
const expectedId = generateTestId(normalizedSchema, test.data, test.valid);
69+
70+
if (test.id !== expectedId) {
71+
mismatchedIdFiles.add(file);
72+
console.log(` ✗ Mismatched ID: ${file}`);
73+
console.log(` Test: ${testCase.description} | ${test.description}`);
74+
console.log(` Current ID: ${test.id}`);
75+
console.log(` Expected ID: ${expectedId}`);
76+
}
77+
78+
if (idMap.has(test.id)) {
79+
const existing = idMap.get(test.id);
80+
duplicateIdFiles.add(file);
81+
duplicateIdFiles.add(existing.file);
82+
console.log(` ✗ Duplicate ID: ${test.id}`);
83+
console.log(` First: ${existing.file} | ${existing.testCase} | ${existing.test}`);
84+
console.log(` Second: ${file} | ${testCase.description} | ${test.description}`);
85+
} else {
86+
idMap.set(test.id, {
87+
file,
88+
testCase: testCase.description,
89+
test: test.description
90+
});
91+
}
92+
}
93+
}
94+
}
95+
96+
console.log("\n" + "=".repeat(60));
97+
console.log("Summary:");
98+
console.log("=".repeat(60));
99+
100+
console.log("\nFiles with missing IDs:");
101+
if (missingIdFiles.size === 0) {
102+
console.log(" ✓ None");
103+
} else {
104+
for (const f of missingIdFiles) console.log(` - ${f}`);
105+
}
106+
107+
console.log("\nFiles with mismatched IDs:");
108+
if (mismatchedIdFiles.size === 0) {
109+
console.log(" ✓ None");
110+
} else {
111+
for (const f of mismatchedIdFiles) console.log(` - ${f}`);
112+
}
113+
114+
console.log("\nFiles with duplicate IDs:");
115+
if (duplicateIdFiles.size === 0) {
116+
console.log(" ✓ None");
117+
} else {
118+
for (const f of duplicateIdFiles) console.log(` - ${f}`);
119+
}
120+
121+
const hasErrors = missingIdFiles.size > 0 || mismatchedIdFiles.size > 0 || duplicateIdFiles.size > 0;
122+
123+
console.log("\n" + "=".repeat(60));
124+
if (hasErrors) {
125+
console.log("❌ Check failed - issues found");
126+
process.exit(1);
127+
} else {
128+
console.log("✅ All checks passed!");
129+
}
130+
}
131+
132+
// Load remotes
133+
const remotesPaths = ["./remotes"];
134+
for (const dialectUri of Object.values(DIALECT_MAP)) {
135+
for (const path of remotesPaths) {
136+
if (fs.existsSync(path)) {
137+
loadRemotes(dialectUri, path);
138+
}
139+
}
140+
}
141+
142+
const dir = process.argv[2] || "tests/draft2020-12";
143+
checkVersion(dir).catch(console.error);

scripts/load-remotes.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// scripts/load-remotes.js
2+
import * as fs from "node:fs";
3+
import { toAbsoluteIri } from "@hyperjump/uri";
4+
import { registerSchema } from "@hyperjump/json-schema/draft-2020-12";
5+
6+
// Keep track of which remote URLs we've already registered
7+
const loadedRemotes = new Set();
8+
9+
export const loadRemotes = (dialectId, filePath, url = "") => {
10+
if (!fs.existsSync(filePath)) {
11+
console.warn(`Warning: Remotes path not found: ${filePath}`);
12+
return;
13+
}
14+
15+
fs.readdirSync(filePath, { withFileTypes: true }).forEach((entry) => {
16+
if (entry.isFile() && entry.name.endsWith(".json")) {
17+
const remotePath = `${filePath}/${entry.name}`;
18+
const remoteUrl = `http://localhost:1234${url}/${entry.name}`;
19+
20+
// If we've already registered this URL once, skip it
21+
if (loadedRemotes.has(remoteUrl)) {
22+
return;
23+
}
24+
25+
const remote = JSON.parse(fs.readFileSync(remotePath, "utf8"));
26+
27+
// Only register if $schema matches dialect OR there's no $schema
28+
if (!remote.$schema || toAbsoluteIri(remote.$schema) === dialectId) {
29+
registerSchema(remote, remoteUrl, dialectId);
30+
loadedRemotes.add(remoteUrl); // ✅ Remember we've registered it
31+
}
32+
} else if (entry.isDirectory()) {
33+
loadRemotes(dialectId, `${filePath}/${entry.name}`, `${url}/${entry.name}`);
34+
}
35+
});
36+
};

0 commit comments

Comments
 (0)