diff --git a/.github/workflows/apply-test-ids.yml b/.github/workflows/apply-test-ids.yml new file mode 100644 index 00000000..ed900467 --- /dev/null +++ b/.github/workflows/apply-test-ids.yml @@ -0,0 +1,70 @@ +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: | + tests: + - 'tests/**/*.json' + other: + - '!tests/**/*.json' + + - name: Block mixed changes + if: steps.changes.outputs.tests == 'true' && 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 + if: steps.changes.outputs.tests == 'true' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node.js + if: steps.changes.outputs.tests == 'true' + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + if: steps.changes.outputs.tests == 'true' + run: npm ci + + - name: Generate test IDs + if: steps.changes.outputs.tests == 'true' + 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 + if: steps.changes.outputs.tests == 'true' + run: | + git config user.name "test-id-bot" + git config user.email "test-id-bot@users.noreply.github.com" + + git add $(find tests -name "*.json" ! -type l) + + 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 }} \ No newline at end of file diff --git a/.github/workflows/generate-test-ids.yml b/.github/workflows/generate-test-ids.yml new file mode 100644 index 00000000..3f6d40e2 --- /dev/null +++ b/.github/workflows/generate-test-ids.yml @@ -0,0 +1,52 @@ +name: Test IDs + +on: [pull_request] + +jobs: + generate: + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} # Check out the PR branch, not the merge commit + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: latest + + - name: Install dependencies + run: npm ci + + # - name: Generate test IDs for v1 + # run: node scripts/generate-ids-for.js v1 + # + # - name: Generate test IDs for draft2020-12 + # run: node scripts/generate-ids-for.js draft2020-12 + # + # - name: Generate test IDs for draft2019-09 + # run: node scripts/generate-ids-for.js draft2019-09 + # + # - name: Generate test IDs for draft7 + # run: node scripts/generate-ids-for.js draft7 + # + # - name: Generate test IDs for draft6 + # run: node scripts/generate-ids-for.js draft6 + # + # - name: Generate test IDs for draft4 + # run: node scripts/generate-ids-for.js draft4 + + - name: Commit and push changes + run: | + git config user.name "test-id-action" + git config user.email "test-id-action@users.noreply.github.com" + git add "tests/**/*.json" + git commit -m "Update test IDs based on the schema, test data, and expected result." || echo "No changes to commit" + git push origin ${{ 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/remotes/draft7/ignore-dependentRequired.json b/remotes/draft2019-09/ignore-dependentRequired.json similarity index 64% rename from remotes/draft7/ignore-dependentRequired.json rename to remotes/draft2019-09/ignore-dependentRequired.json index 0ea927b5..f2eb5048 100644 --- a/remotes/draft7/ignore-dependentRequired.json +++ b/remotes/draft2019-09/ignore-dependentRequired.json @@ -1,7 +1,7 @@ { - "$id": "http://localhost:1234/draft7/integer.json", + "$id": "http://localhost:1234/draft2019-09/integer.json", "$schema": "http://json-schema.org/draft-07/schema#", "dependentRequired": { "foo": ["bar"] } -} \ No newline at end of file +} diff --git a/remotes/draft2020-12/prefixItems.json b/remotes/draft2019-09/prefixItems.json similarity index 100% rename from remotes/draft2020-12/prefixItems.json rename to remotes/draft2019-09/prefixItems.json diff --git a/remotes/draft2019-09/ignore-prefixItems.json b/remotes/draft2020-12/ignore-prefixItems.json similarity index 67% rename from remotes/draft2019-09/ignore-prefixItems.json rename to remotes/draft2020-12/ignore-prefixItems.json index b5ef3928..0ef44663 100644 --- a/remotes/draft2019-09/ignore-prefixItems.json +++ b/remotes/draft2020-12/ignore-prefixItems.json @@ -1,5 +1,5 @@ { - "$id": "http://localhost:1234/draft2019-09/ignore-prefixItems.json", + "$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/draft2019-09/dependentRequired.json b/remotes/draft7/dependentRequired.json similarity index 63% rename from remotes/draft2019-09/dependentRequired.json rename to remotes/draft7/dependentRequired.json index 0d691d96..f16a3450 100644 --- a/remotes/draft2019-09/dependentRequired.json +++ b/remotes/draft7/dependentRequired.json @@ -1,5 +1,5 @@ { - "$id": "http://localhost:1234/draft2019-09/dependentRequired.json", + "$id": "http://localhost:1234/draft7/dependentRequired.json", "$schema": "https://json-schema.org/draft/2019-09/schema", "dependentRequired": { "foo": ["bar"] diff --git a/remotes/v1/meta-schema.json b/remotes/v1/meta-schema.json new file mode 100644 index 00000000..e7eb6ba9 --- /dev/null +++ b/remotes/v1/meta-schema.json @@ -0,0 +1,204 @@ +{ + "$schema": "https://json-schema.org/v1", + "$id": "https://json-schema.org/v1/2026", + "$dynamicAnchor": "meta", + + "title": "JSON Schema Core and Validation specification meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "$ref": "#/$defs/iriReferenceString", + "$comment": "Fragments not allowed.", + "pattern": "^[^#]*$" + }, + "$schema": { "$ref": "#/$defs/iriString" }, + "$ref": { "$ref": "#/$defs/iriReferenceString" }, + "$anchor": { "$ref": "#/$defs/anchorString" }, + "$dynamicRef": { "$ref": "#/$defs/anchorString" }, + "$dynamicAnchor": { "$ref": "#/$defs/anchorString" }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$dynamicRef": "meta" } + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "prefixItems": { "$ref": "#/$defs/schemaArray" }, + "items": { "$dynamicRef": "meta" }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "contains": { "$dynamicRef": "meta" }, + "additionalProperties": { "$dynamicRef": "meta" }, + "properties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "meta" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "meta" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { "$dynamicRef": "meta" }, + "default": {} + }, + "propertyNames": { "$dynamicRef": "meta" }, + "if": { "$dynamicRef": "meta" }, + "then": { "$dynamicRef": "meta" }, + "else": { "$dynamicRef": "meta" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$dynamicRef": "meta" }, + "unevaluatedItems": { "$dynamicRef": "meta" }, + "unevaluatedProperties": { "$dynamicRef": "meta" }, + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + }, + "format": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentSchema": { "$dynamicRef": "meta" }, + + "$vocabulary": { + "$comment": "Proposed keyword: https://github.com/json-schema-org/json-schema-spec/blob/main/specs/proposals/vocabularies.md" + }, + "propertyDependencies": { + "$comment": "Proposed keyword: https://github.com/json-schema-org/json-schema-spec/blob/main/specs/proposals/propertyDependencies.md" + } + }, + "patternProperties": { + "^x-": true + }, + "propertyNames": { + "pattern": "^[^$]|^\\$(id|schema|ref|anchor|dynamicRef|dynamicAnchor|comment|defs)$" + }, + "$dynamicRef": "extension", + "unevaluatedProperties": false, + "$defs": { + "extension": { + "$dynamicAnchor": "extension" + }, + "anchorString": { + "type": "string", + "pattern": "^[A-Za-z_][-A-Za-z0-9._]*$" + }, + "iriString": { + "type": "string", + "format": "iri" + }, + "iriReferenceString": { + "type": "string", + "format": "iri-reference" + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$dynamicRef": "meta" } + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} diff --git a/scripts/add-test-ids.js b/scripts/add-test-ids.js new file mode 100644 index 00000000..422d2415 --- /dev/null +++ b/scripts/add-test-ids.js @@ -0,0 +1,89 @@ +import * as fs from "node:fs"; + +import { parse, modify, applyEdits } from "jsonc-parser"; +import { loadRemotes, generateTestId, normalizeSchema } from "./utils/test-ids.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#" +}; + + + + +async function addIdsToFile(filePath, dialectUri) { + console.log("Reading:", filePath); + + const text = fs.readFileSync(filePath, "utf8"); + const tests = parse(text); + let edits = []; + let added = 0; + + for (let i = 0; i < tests.length; i++) { + const testCase = tests[i]; + const normalizedSchema = await normalizeSchema(testCase.schema, dialectUri); + + for (let j = 0; j < testCase.tests.length; j++) { + const test = testCase.tests[j]; + + if (!test.id) { + const id = generateTestId( + normalizedSchema, + test.data, + test.valid + ); + + const path = [i, "tests", j, "id"]; + + edits.push( + ...modify(text, path, id, { + formattingOptions: { + insertSpaces: true, + tabSize: 4 + } + }) + ); + + added++; + } + } + } + + if (added > 0) { + const updatedText = applyEdits(text, edits); + fs.writeFileSync(filePath, updatedText); + console.log(` Added ${added} IDs`); + } else { + console.log(" All tests already have IDs"); + } +} + +//CLI stuff + +const dialectArg = process.argv[2]; +if (!dialectArg || !DIALECT_MAP[dialectArg]) { + console.error("Usage: node add-test-ids.js [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) { + await addIdsToFile(filePath, dialectUri); +} else { + const testDir = `tests/${dialectArg}`; + const files = fs.readdirSync(testDir).filter(f => f.endsWith(".json")); + + for (const file of files) { + await addIdsToFile(`${testDir}/${file}`, dialectUri); + } +} diff --git a/scripts/check-test-ids.js b/scripts/check-test-ids.js new file mode 100644 index 00000000..e69de29b 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/load-remotes.js b/scripts/load-remotes.js new file mode 100644 index 00000000..eec8ba77 --- /dev/null +++ b/scripts/load-remotes.js @@ -0,0 +1,35 @@ +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}`); + } + }); +}; \ No newline at end of file diff --git a/scripts/normalize.js b/scripts/normalize.js new file mode 100644 index 00000000..94ffeb91 --- /dev/null +++ b/scripts/normalize.js @@ -0,0 +1,203 @@ +import * as Schema from "@hyperjump/browser"; +import * as Pact from "@hyperjump/pact"; +import * as JsonPointer from "@hyperjump/json-pointer"; +import { toAbsoluteIri } from "@hyperjump/uri"; +import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12"; +import { getSchema, getKeywordId } from "@hyperjump/json-schema/experimental"; +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 sanitizeTopLevelId = (schema) => { + if (typeof schema !== "object" || schema === null) return schema; + const copy = { ...schema }; + if (typeof copy.$id === "string" && copy.$id.startsWith("file:")) { + copy.$id = copy.$id.replace(/^file:/, "x-file:"); + } + return copy; +}; +// =========================================== + + +export const normalize = async (rawSchema, dialectUri) => { + const schemaUri = "https://test-suite.json-schema.org/main"; + + + const safeSchema = sanitizeTopLevelId(rawSchema); + + try { + + registerSchema(safeSchema, schemaUri, dialectUri); + + const schema = await getSchema(schemaUri); + const ast = { metaData: {} }; + await compile(schema, ast); + return ast; + } finally { + unregisterSchema(schemaUri); + } +}; + +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, // base dynamicRef + + "https://json-schema.org/keyword/draft-2020-12/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/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/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/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 +}; \ No newline at end of file diff --git a/scripts/utils/generateTestIds.js b/scripts/utils/generateTestIds.js new file mode 100644 index 00000000..45a3dbb7 --- /dev/null +++ b/scripts/utils/generateTestIds.js @@ -0,0 +1,13 @@ +import * as crypto from "node:crypto"; +import jsonStringify from "json-stringify-deterministic"; + +export default function generateTestId(normalizedSchema, testData, testValid) { + return crypto + .createHash("md5") + .update( + jsonStringify(normalizedSchema) + + jsonStringify(testData) + + testValid + ) + .digest("hex"); +} \ No newline at end of file diff --git a/scripts/utils/jsonfiles.js b/scripts/utils/jsonfiles.js new file mode 100644 index 00000000..b4c41a08 --- /dev/null +++ b/scripts/utils/jsonfiles.js @@ -0,0 +1,11 @@ + +export default 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; + } + } +} 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..5ff6d9ae 100644 --- a/test-schema.json +++ b/test-schema.json @@ -9,7 +9,7 @@ "description": "An individual test case, containing multiple tests of a single schema's behavior", "type": "object", - "required": [ "description", "schema", "tests" ], + "required": ["description", "schema", "tests"], "properties": { "description": { "description": "The test case description", @@ -28,53 +28,53 @@ "items": { "$ref": "#/$defs/test" }, "minItems": 1 }, - "specification":{ + "specification": { "description": "A reference to a specification document which defines the behavior tested by this test case. Typically this should be a JSON Schema specification document, though in cases where the JSON Schema specification points to another RFC it should contain *both* the portion of the JSON Schema specification which indicates what RFC (and section) to follow as *well* as information on where in that specification the behavior is specified.", "type": "array", "minItems": 1, "uniqueItems": true, - "items":{ + "items": { "properties": { - "core": { + "core": { "description": "A section in official JSON Schema core drafts", "url": "https://json-schema.org/specification-links", - "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", - "type":"string" + "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", + "type": "string" }, "validation": { "description": "A section in official JSON Schema validation drafts", "url": "https://json-schema.org/specification-links", - "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", - "type":"string" + "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", + "type": "string" }, - "ecma262": { + "ecma262": { "description": "A section in official ECMA 262 specification for defining regular expressions", "url": "https://262.ecma-international.org/", - "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", - "type":"string" + "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", + "type": "string" }, - "perl5": { + "perl5": { "description": "A section name in Perl documentation for defining regular expressions", "url": "https://perldoc.perl.org/perlre", - "type":"string" + "type": "string" }, - "quote": { + "quote": { "description": "Quote describing the test case", - "type":"string" + "type": "string" } }, "patternProperties": { - "^rfc\\d+$": { + "^rfc\\d+$": { "description": "A section in official RFC for the given rfc number", "url": "https://www.rfc-editor.org/", - "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", - "type":"string" + "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", + "type": "string" }, - "^iso\\d+$": { + "^iso\\d+$": { "description": "A section in official ISO for the given iso number", - "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", - "type": "string" + "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", + "type": "string" } }, "additionalProperties": { "type": "string" }, @@ -85,7 +85,7 @@ "pattern": "^((iso)|(rfc))[0-9]+$" }, { - "enum": [ "core", "validation", "ecma262", "perl5", "quote" ] + "enum": ["core", "validation", "ecma262", "perl5", "quote"] } ] } @@ -100,7 +100,7 @@ "description": "A single test", "type": "object", - "required": [ "description", "data", "valid" ], + "required": ["description", "data", "valid"], "properties": { "description": { "description": "The test description, briefly explaining which behavior it exercises", @@ -116,9 +116,13 @@ "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 } } -} +} \ No newline at end of file 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/enum.json b/tests/draft2020-12/enum.json index c8f35eac..15d26c2e 100644 --- a/tests/draft2020-12/enum.json +++ b/tests/draft2020-12/enum.json @@ -9,12 +9,14 @@ { "description": "one of the enum is valid", "data": 1, - "valid": true + "valid": true, + "id": "bc16cb75d14903a732326a24d1416757" }, { "description": "something else is invalid", "data": 4, - "valid": false + "valid": false, + "id": "2ea4a168ef00d32d444b3d49dc5a617d" } ] }, @@ -28,27 +30,32 @@ { "description": "one of the enum is valid", "data": [], - "valid": true + "valid": true, + "id": "e31a02b023906271a7f40576a03df0de" }, { "description": "something else is invalid", "data": null, - "valid": false + "valid": false, + "id": "b47e0170004d316b00a5151b0d2b566c" }, { "description": "objects are deep compared", "data": {"foo": false}, - "valid": false + "valid": false, + "id": "7285822674e57f31de9c7280fa9d8900" }, { "description": "valid object matches", "data": {"foo": 12}, - "valid": true + "valid": true, + "id": "27ffbacf5774ff2354458a6954ed1591" }, { "description": "extra properties in object is invalid", "data": {"foo": 12, "boo": 42}, - "valid": false + "valid": false, + "id": "e220c92cdef194c74c49f9b7eb86b211" } ] }, @@ -62,17 +69,20 @@ { "description": "null is valid", "data": null, - "valid": true + "valid": true, + "id": "326f454f3db2a5fb76d797b43c812285" }, { "description": "number is valid", "data": 6, - "valid": true + "valid": true, + "id": "9884c0daef3095b4ff88c309bc87a620" }, { "description": "something else is invalid", "data": "test", - "valid": false + "valid": false, + "id": "4032c1754a28b94e914f8dfea1e12a26" } ] }, @@ -91,32 +101,38 @@ { "description": "both properties are valid", "data": {"foo":"foo", "bar":"bar"}, - "valid": true + "valid": true, + "id": "dba127f9663272184ec3ea3072676b4d" }, { "description": "wrong foo value", "data": {"foo":"foot", "bar":"bar"}, - "valid": false + "valid": false, + "id": "0f0cca9923128d29922561fe7d92a436" }, { "description": "wrong bar value", "data": {"foo":"foo", "bar":"bart"}, - "valid": false + "valid": false, + "id": "d32cef9094b8a87100a57cee099310d4" }, { "description": "missing optional property is valid", "data": {"bar":"bar"}, - "valid": true + "valid": true, + "id": "23a7c86ef0b1e3cac9ebb6175de888d0" }, { "description": "missing required property is invalid", "data": {"foo":"foo"}, - "valid": false + "valid": false, + "id": "61ac5943d52ba688c77e26a1a7b5d174" }, { "description": "missing all properties is invalid", "data": {}, - "valid": false + "valid": false, + "id": "8b7be28a144634955b915ad780f4f2a5" } ] }, @@ -130,17 +146,20 @@ { "description": "member 1 is valid", "data": "foo\nbar", - "valid": true + "valid": true, + "id": "056ae2b9aad469dd32a63b1c97716c6e" }, { "description": "member 2 is valid", "data": "foo\rbar", - "valid": true + "valid": true, + "id": "ade764a27bb0fed294cefb1b6b275cd5" }, { "description": "another string is invalid", "data": "abc", - "valid": false + "valid": false, + "id": "1faca62e488e50dc47de0b3a26751477" } ] }, @@ -154,17 +173,20 @@ { "description": "false is valid", "data": false, - "valid": true + "valid": true, + "id": "4e5b4da53732e4fdeaa5ed74127363bc" }, { "description": "integer zero is invalid", "data": 0, - "valid": false + "valid": false, + "id": "24f12f5bf7ae6be32a9634be17835176" }, { "description": "float zero is invalid", "data": 0.0, - "valid": false + "valid": false, + "id": "24f12f5bf7ae6be32a9634be17835176" } ] }, @@ -178,17 +200,20 @@ { "description": "[false] is valid", "data": [false], - "valid": true + "valid": true, + "id": "9fe1fb5471d006782fe7ead4aa9909fa" }, { "description": "[0] is invalid", "data": [0], - "valid": false + "valid": false, + "id": "8fc4591c8c9ddf9bd41a234d5fa24521" }, { "description": "[0.0] is invalid", "data": [0.0], - "valid": false + "valid": false, + "id": "8fc4591c8c9ddf9bd41a234d5fa24521" } ] }, @@ -202,17 +227,20 @@ { "description": "true is valid", "data": true, - "valid": true + "valid": true, + "id": "71738e4d71680e268bbee31fc43a4932" }, { "description": "integer one is invalid", "data": 1, - "valid": false + "valid": false, + "id": "e8901b103bcc1340391efd3e02420500" }, { "description": "float one is invalid", "data": 1.0, - "valid": false + "valid": false, + "id": "e8901b103bcc1340391efd3e02420500" } ] }, @@ -226,17 +254,20 @@ { "description": "[true] is valid", "data": [true], - "valid": true + "valid": true, + "id": "44049e91dee93b7dafd9cdbc0156a604" }, { "description": "[1] is invalid", "data": [1], - "valid": false + "valid": false, + "id": "f8b1b069fd03b04f07f5b70786cb916c" }, { "description": "[1.0] is invalid", "data": [1.0], - "valid": false + "valid": false, + "id": "f8b1b069fd03b04f07f5b70786cb916c" } ] }, @@ -250,17 +281,20 @@ { "description": "false is invalid", "data": false, - "valid": false + "valid": false, + "id": "5cedc4948cc5d6744fa620039487ac15" }, { "description": "integer zero is valid", "data": 0, - "valid": true + "valid": true, + "id": "133e04205807df77ac09e730c237aba4" }, { "description": "float zero is valid", "data": 0.0, - "valid": true + "valid": true, + "id": "133e04205807df77ac09e730c237aba4" } ] }, @@ -274,17 +308,20 @@ { "description": "[false] is invalid", "data": [false], - "valid": false + "valid": false, + "id": "e6da8d988a7c265913e24eaed91af3de" }, { "description": "[0] is valid", "data": [0], - "valid": true + "valid": true, + "id": "10ba58a9539f8558892d28d2f5e3493d" }, { "description": "[0.0] is valid", "data": [0.0], - "valid": true + "valid": true, + "id": "10ba58a9539f8558892d28d2f5e3493d" } ] }, @@ -298,17 +335,20 @@ { "description": "true is invalid", "data": true, - "valid": false + "valid": false, + "id": "a8356b24823e955bc3895c25b9e442eb" }, { "description": "integer one is valid", "data": 1, - "valid": true + "valid": true, + "id": "603d6607a05072c21bcbff4578494e22" }, { "description": "float one is valid", "data": 1.0, - "valid": true + "valid": true, + "id": "603d6607a05072c21bcbff4578494e22" } ] }, @@ -322,17 +362,20 @@ { "description": "[true] is invalid", "data": [true], - "valid": false + "valid": false, + "id": "732eec1f5b7fdf6d24c40d77072628e3" }, { "description": "[1] is valid", "data": [1], - "valid": true + "valid": true, + "id": "8064c0569b7c7a17631d0dc802090303" }, { "description": "[1.0] is valid", "data": [1.0], - "valid": true + "valid": true, + "id": "8064c0569b7c7a17631d0dc802090303" } ] }, @@ -346,12 +389,14 @@ { "description": "match string with nul", "data": "hello\u0000there", - "valid": true + "valid": true, + "id": "d94ca2719908ab9c789d7c71e4cc93e4" }, { "description": "do not match string lacking nul", "data": "hellothere", - "valid": false + "valid": false, + "id": "a2c7a2aa3202a473dc03b5a8fcc070e9" } ] } 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/defs.json b/tests/v1/defs.json index 8e167655..f7434598 100644 --- a/tests/v1/defs.json +++ b/tests/v1/defs.json @@ -3,7 +3,7 @@ "description": "validate definition against metaschema", "schema": { "$schema": "https://json-schema.org/v1", - "$ref": "https://json-schema.org/v1" + "$ref": "http://localhost:1234/v1/meta-schema.json" }, "tests": [ { diff --git a/tests/v1/ref.json b/tests/v1/ref.json index e7a36f68..4c395ce0 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": "https://json-schema.org/v1" + "$ref": "http://localhost:1234/v1/meta-schema.json" }, "tests": [ { @@ -829,7 +829,7 @@ "schema": { "$schema": "https://json-schema.org/v1", "$comment": "RFC 8141 ยง2.3.3, but we don't allow fragments", - "$ref": "https://json-schema.org/v1" + "$ref": "http://localhost:1234/v1/meta-schema.json" }, "tests": [ {