-
-
Notifications
You must be signed in to change notification settings - Fork 231
Add normalized hash-based test IDs to draft2020-12/enum.json (POC for #698) #796
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,21 @@ | ||
| { | ||
| "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": [ | ||
| "json-schema", | ||
| "tests" | ||
| ], | ||
| "author": "http://json-schema.org", | ||
| "license": "MIT" | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "@hyperjump/browser": "^1.3.1", | ||
| "@hyperjump/json-pointer": "^1.1.1", | ||
| "@hyperjump/json-schema": "^1.17.2", | ||
| "@hyperjump/pact": "^1.4.0", | ||
| "@hyperjump/uri": "^1.3.2", | ||
| "json-stringify-deterministic": "^1.0.12" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import * as fs from "node:fs"; | ||
| import * as crypto from "node:crypto"; | ||
| import jsonStringify from "json-stringify-deterministic"; | ||
| import { normalize } from "./normalize.js"; | ||
| import { loadRemotes } from "./load-remotes.js"; | ||
|
|
||
| const DIALECT_MAP = { | ||
| "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#" | ||
| }; | ||
|
|
||
| function generateTestId(normalizedSchema, testData, testValid) { | ||
| return crypto | ||
| .createHash("md5") | ||
| .update(jsonStringify(normalizedSchema) + jsonStringify(testData) + testValid) | ||
| .digest("hex"); | ||
| } | ||
|
|
||
| async function addIdsToFile(filePath, dialectUri) { | ||
| console.log("Reading:", filePath); | ||
| const tests = JSON.parse(fs.readFileSync(filePath, "utf8")); | ||
| let added = 0; | ||
|
|
||
| for (const testCase of tests) { | ||
| // Pass dialectUri from directory, not from schema | ||
| // @hyperjump/json-schema handles the schema's $schema internally | ||
| const normalizedSchema = await normalize(testCase.schema, dialectUri); | ||
|
|
||
| for (const test of testCase.tests) { | ||
| if (!test.id) { | ||
| test.id = generateTestId(normalizedSchema, test.data, test.valid); | ||
| added++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (added > 0) { | ||
| fs.writeFileSync(filePath, JSON.stringify(tests, null, 4) + "\n"); | ||
| console.log(` Added ${added} IDs`); | ||
| } else { | ||
| console.log(" All tests already have IDs"); | ||
| } | ||
| } | ||
|
|
||
| // Get dialect from command line argument (e.g., "draft2020-12") | ||
| const dialectArg = process.argv[2]; | ||
| if (!dialectArg || !DIALECT_MAP[dialectArg]) { | ||
| console.error("Usage: node add-test-ids.js <dialect> [file-path]"); | ||
| console.error("Available dialects:", Object.keys(DIALECT_MAP).join(", ")); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const dialectUri = DIALECT_MAP[dialectArg]; | ||
| const filePath = process.argv[3]; | ||
|
|
||
| // Load remotes only for the specified dialect | ||
| loadRemotes(dialectUri, "./remotes"); | ||
|
|
||
| if (filePath) { | ||
| // Process single file | ||
| addIdsToFile(filePath, dialectUri); | ||
| } else { | ||
| // Process all files in the dialect directory | ||
| const testDir = `tests/${dialectArg}`; | ||
| const files = fs.readdirSync(testDir).filter(f => f.endsWith('.json')); | ||
|
|
||
| for (const file of files) { | ||
| await addIdsToFile(`${testDir}/${file}`, dialectUri); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,143 @@ | ||||||
| import * as fs from "node:fs"; | ||||||
| import * as path from "node:path"; | ||||||
| import * as crypto from "node:crypto"; | ||||||
| import jsonStringify from "json-stringify-deterministic"; | ||||||
| import { normalize } from "./normalize.js"; | ||||||
| import { loadRemotes } from "./load-remotes.js"; | ||||||
|
|
||||||
| const DIALECT_MAP = { | ||||||
| "https://json-schema.org/draft/2020-12/schema": "https://json-schema.org/draft/2020-12/schema", | ||||||
| "https://json-schema.org/draft/2019-09/schema": "https://json-schema.org/draft/2019-09/schema", | ||||||
| "http://json-schema.org/draft-07/schema#": "http://json-schema.org/draft-07/schema#", | ||||||
| "http://json-schema.org/draft-06/schema#": "http://json-schema.org/draft-06/schema#", | ||||||
| "http://json-schema.org/draft-04/schema#": "http://json-schema.org/draft-04/schema#" | ||||||
| }; | ||||||
|
|
||||||
| function* jsonFiles(dir) { | ||||||
| for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { | ||||||
| const full = path.join(dir, entry.name); | ||||||
| if (entry.isDirectory()) { | ||||||
| yield* jsonFiles(full); | ||||||
| } else if (entry.isFile() && entry.name.endsWith(".json")) { | ||||||
| yield full; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| function getDialectUri(schema) { | ||||||
| if (schema.$schema && DIALECT_MAP[schema.$schema]) { | ||||||
| return DIALECT_MAP[schema.$schema]; | ||||||
| } | ||||||
| return "https://json-schema.org/draft/2020-12/schema"; | ||||||
| } | ||||||
|
|
||||||
| function generateTestId(normalizedSchema, testData, testValid) { | ||||||
| return crypto | ||||||
| .createHash("md5") | ||||||
| .update(jsonStringify(normalizedSchema) + jsonStringify(testData) + testValid) | ||||||
| .digest("hex"); | ||||||
| } | ||||||
|
|
||||||
| async function checkVersion(dir) { | ||||||
| const missingIdFiles = new Set(); | ||||||
| const duplicateIdFiles = new Set(); | ||||||
| const mismatchedIdFiles = new Set(); | ||||||
| const idMap = new Map(); | ||||||
|
|
||||||
| console.log(`Checking tests in ${dir}...`); | ||||||
|
|
||||||
| for (const file of jsonFiles(dir)) { | ||||||
| const tests = JSON.parse(fs.readFileSync(file, "utf8")); | ||||||
|
|
||||||
| for (let i = 0; i < tests.length; i++) { | ||||||
| const testCase = tests[i]; | ||||||
| if (!Array.isArray(testCase.tests)) continue; | ||||||
|
|
||||||
| const dialectUri = getDialectUri(testCase.schema || {}); | ||||||
| const normalizedSchema = await normalize(testCase.schema || true, dialectUri); | ||||||
|
Comment on lines
+56
to
+57
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Also, |
||||||
|
|
||||||
| for (let j = 0; j < testCase.tests.length; j++) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a
Suggested change
|
||||||
| const test = testCase.tests[j]; | ||||||
|
|
||||||
| if (!test.id) { | ||||||
| missingIdFiles.add(file); | ||||||
| console.log(` ✗ Missing ID: ${file} | ${testCase.description} | ${test.description}`); | ||||||
| continue; | ||||||
| } | ||||||
|
|
||||||
| const expectedId = generateTestId(normalizedSchema, test.data, test.valid); | ||||||
|
|
||||||
| if (test.id !== expectedId) { | ||||||
| mismatchedIdFiles.add(file); | ||||||
| console.log(` ✗ Mismatched ID: ${file}`); | ||||||
| console.log(` Test: ${testCase.description} | ${test.description}`); | ||||||
| console.log(` Current ID: ${test.id}`); | ||||||
| console.log(` Expected ID: ${expectedId}`); | ||||||
| } | ||||||
|
|
||||||
| if (idMap.has(test.id)) { | ||||||
| const existing = idMap.get(test.id); | ||||||
| duplicateIdFiles.add(file); | ||||||
| duplicateIdFiles.add(existing.file); | ||||||
| console.log(` ✗ Duplicate ID: ${test.id}`); | ||||||
| console.log(` First: ${existing.file} | ${existing.testCase} | ${existing.test}`); | ||||||
| console.log(` Second: ${file} | ${testCase.description} | ${test.description}`); | ||||||
| } else { | ||||||
| idMap.set(test.id, { | ||||||
| file, | ||||||
| testCase: testCase.description, | ||||||
| test: test.description | ||||||
| }); | ||||||
| } | ||||||
|
Comment on lines
+78
to
+91
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking that we probably don't need to check for duplicate IDs. Checking for duplicates would be necessary if people were assigning IDs, but these ids are generated based on the content of the test, so there should never be duplicates. If someone uses the ID from another test by copy-paste mistake, we should get a Mismatched ID error, so we don't need a duplicate check as well. |
||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| console.log("\n" + "=".repeat(60)); | ||||||
| console.log("Summary:"); | ||||||
| console.log("=".repeat(60)); | ||||||
|
|
||||||
| console.log("\nFiles with missing IDs:"); | ||||||
| if (missingIdFiles.size === 0) { | ||||||
| console.log(" ✓ None"); | ||||||
| } else { | ||||||
| for (const f of missingIdFiles) console.log(` - ${f}`); | ||||||
| } | ||||||
|
|
||||||
| console.log("\nFiles with mismatched IDs:"); | ||||||
| if (mismatchedIdFiles.size === 0) { | ||||||
| console.log(" ✓ None"); | ||||||
| } else { | ||||||
| for (const f of mismatchedIdFiles) console.log(` - ${f}`); | ||||||
| } | ||||||
|
|
||||||
| console.log("\nFiles with duplicate IDs:"); | ||||||
| if (duplicateIdFiles.size === 0) { | ||||||
| console.log(" ✓ None"); | ||||||
| } else { | ||||||
| for (const f of duplicateIdFiles) console.log(` - ${f}`); | ||||||
| } | ||||||
|
|
||||||
| const hasErrors = missingIdFiles.size > 0 || mismatchedIdFiles.size > 0 || duplicateIdFiles.size > 0; | ||||||
|
|
||||||
| console.log("\n" + "=".repeat(60)); | ||||||
| if (hasErrors) { | ||||||
| console.log("❌ Check failed - issues found"); | ||||||
| process.exit(1); | ||||||
| } else { | ||||||
| console.log("✅ All checks passed!"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Load remotes | ||||||
| const remotesPaths = ["./remotes"]; | ||||||
| for (const dialectUri of Object.values(DIALECT_MAP)) { | ||||||
| for (const path of remotesPaths) { | ||||||
| if (fs.existsSync(path)) { | ||||||
| loadRemotes(dialectUri, path); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| const dir = process.argv[2] || "tests/draft2020-12"; | ||||||
| checkVersion(dir).catch(console.error); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need to catch if you're not doing to something meaningful to recover from the error. You can just let if throw.
Suggested change
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // scripts/load-remotes.js | ||
| import * as fs from "node:fs"; | ||
| import { toAbsoluteIri } from "@hyperjump/uri"; | ||
| import { registerSchema } from "@hyperjump/json-schema/draft-2020-12"; | ||
|
|
||
| // Keep track of which remote URLs we've already registered | ||
| const loadedRemotes = new Set(); | ||
|
|
||
| export const loadRemotes = (dialectId, filePath, url = "") => { | ||
| if (!fs.existsSync(filePath)) { | ||
| console.warn(`Warning: Remotes path not found: ${filePath}`); | ||
| return; | ||
| } | ||
|
|
||
| fs.readdirSync(filePath, { withFileTypes: true }).forEach((entry) => { | ||
| if (entry.isFile() && entry.name.endsWith(".json")) { | ||
| const remotePath = `${filePath}/${entry.name}`; | ||
| const remoteUrl = `http://localhost:1234${url}/${entry.name}`; | ||
|
|
||
| // If we've already registered this URL once, skip it | ||
| if (loadedRemotes.has(remoteUrl)) { | ||
| return; | ||
| } | ||
|
|
||
| const remote = JSON.parse(fs.readFileSync(remotePath, "utf8")); | ||
|
|
||
| // Only register if $schema matches dialect OR there's no $schema | ||
| if (!remote.$schema || toAbsoluteIri(remote.$schema) === dialectId) { | ||
| registerSchema(remote, remoteUrl, dialectId); | ||
| loadedRemotes.add(remoteUrl); // ✅ Remember we've registered it | ||
| } | ||
| } else if (entry.isDirectory()) { | ||
| loadRemotes(dialectId, `${filePath}/${entry.name}`, `${url}/${entry.name}`); | ||
| } | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, that's useless 😄