diff --git a/package.json b/package.json index 75da9e29..67058746 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,13 @@ "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" + } } diff --git a/scripts/add-test-ids.js b/scripts/add-test-ids.js new file mode 100644 index 00000000..295414e2 --- /dev/null +++ b/scripts/add-test-ids.js @@ -0,0 +1,74 @@ +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 = { + "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 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 addIdsToFile(filePath) { + console.log("Reading:", filePath); + const tests = JSON.parse(fs.readFileSync(filePath, "utf8")); + let changed = false; + let added = 0; + + if (!Array.isArray(tests)) { + console.log("Expected an array at top level, got:", typeof tests); + return; + } + + for (const testCase of tests) { + if (!Array.isArray(testCase.tests)) continue; + + const dialectUri = getDialectUri(testCase.schema || {}); + const normalizedSchema = await normalize(testCase.schema || true, dialectUri); + + for (const test of testCase.tests) { + if (!test.id) { + test.id = generateTestId(normalizedSchema, test.data, test.valid); + changed = true; + added++; + } + } + } + + if (changed) { + fs.writeFileSync(filePath, JSON.stringify(tests, null, 2) + "\n"); + console.log(`✓ Added ${added} IDs`); + } else { + console.log("✓ All tests already have IDs"); + } +} + +// Load remotes for all dialects +const remotesPaths = ["./remotes"]; +for (const dialectUri of Object.values(DIALECT_MAP)) { + for (const path of remotesPaths) { + if (fs.existsSync(path)) { + loadRemotes(dialectUri, path); + } + } +} + +const filePath = process.argv[2] || "tests/draft2020-12/enum.json"; +addIdsToFile(filePath).catch(console.error); \ No newline at end of file diff --git a/scripts/check-test-ids.js b/scripts/check-test-ids.js new file mode 100644 index 00000000..feed6513 --- /dev/null +++ b/scripts/check-test-ids.js @@ -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); + + for (let j = 0; j < testCase.tests.length; j++) { + 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 + }); + } + } + } + } + + 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); \ No newline at end of file diff --git a/scripts/load-remotes.js b/scripts/load-remotes.js new file mode 100644 index 00000000..f7ab7aff --- /dev/null +++ b/scripts/load-remotes.js @@ -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}`); + } + }); +}; diff --git a/scripts/normalize.js b/scripts/normalize.js new file mode 100644 index 00000000..bad333fc --- /dev/null +++ b/scripts/normalize.js @@ -0,0 +1,212 @@ +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"; + + +// =========================================== +// CHANGE #2 (ADDED): sanitize file:// $id +// =========================================== +const sanitizeTopLevelId = (schema) => { + if (typeof schema !== "object" || schema === null) return schema; + const copy = { ...schema }; + if (typeof copy.$id === "string" && copy.$id.startsWith("file:")) { + delete copy.$id; + } + return copy; +}; +// =========================================== + + +export const normalize = async (rawSchema, dialectUri) => { + const schemaUri = "https://test-suite.json-schema.org/main"; + + // =========================================== + // CHANGE #2 (APPLIED HERE) + // =========================================== + const safeSchema = sanitizeTopLevelId(rawSchema); + // =========================================== + + try { + // BEFORE: registerSchema(rawSchema, schemaUri, dialectUri) + 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 + + // =========================================== + // CHANGE #1 (ADDED): draft-2020-12/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 +}; diff --git a/test-schema.json b/test-schema.json index 0087c5e3..fee5a644 100644 --- a/test-schema.json +++ b/test-schema.json @@ -1,124 +1,128 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://json-schema.org/tests/test-schema", - "description": "A schema for files contained within this suite", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/tests/test-schema", + "description": "A schema for files contained within this suite", - "type": "array", - "minItems": 1, - "items": { - "description": "An individual test case, containing multiple tests of a single schema's behavior", + "type": "array", + "minItems": 1, + "items": { + "description": "An individual test case, containing multiple tests of a single schema's behavior", - "type": "object", - "required": [ "description", "schema", "tests" ], - "properties": { - "description": { - "description": "The test case description", - "type": "string" + "type": "object", + "required": ["description", "schema", "tests"], + "properties": { + "description": { + "description": "The test case description", + "type": "string" + }, + "comment": { + "description": "Any additional comments about the test case", + "type": "string" + }, + "schema": { + "description": "A valid JSON Schema (one written for the corresponding version directory that the file sits within)." + }, + "tests": { + "description": "A set of related tests all using the same schema", + "type": "array", + "items": { "$ref": "#/$defs/test" }, + "minItems": 1 + }, + "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": { + "properties": { + "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" }, - "comment": { - "description": "Any additional comments about the test case", - "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" }, - "schema": { - "description": "A valid JSON Schema (one written for the corresponding version directory that the file sits within)." + "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" }, - "tests": { - "description": "A set of related tests all using the same schema", - "type": "array", - "items": { "$ref": "#/$defs/test" }, - "minItems": 1 + "perl5": { + "description": "A section name in Perl documentation for defining regular expressions", + "url": "https://perldoc.perl.org/perlre", + "type": "string" }, - "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":{ - "properties": { - "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" - }, - "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" - }, - "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" - }, - "perl5": { - "description": "A section name in Perl documentation for defining regular expressions", - "url": "https://perldoc.perl.org/perlre", - "type":"string" - }, - "quote": { - "description": "Quote describing the test case", - "type":"string" - } - }, - "patternProperties": { - "^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" - }, - "^iso\\d+$": { - "description": "A section in official ISO for the given iso number", - "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", - "type": "string" - } - }, - "additionalProperties": { "type": "string" }, - "minProperties": 1, - "propertyNames": { - "oneOf": [ - { - "pattern": "^((iso)|(rfc))[0-9]+$" - }, - { - "enum": [ "core", "validation", "ecma262", "perl5", "quote" ] - } - ] - } - } + "quote": { + "description": "Quote describing the test case", + "type": "string" } - }, - "additionalProperties": false + }, + "patternProperties": { + "^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" + }, + "^iso\\d+$": { + "description": "A section in official ISO for the given iso number", + "pattern": "^[0-9a-zA-Z]+(\\.[0-9a-zA-Z]+)*$", + "type": "string" + } + }, + "additionalProperties": { "type": "string" }, + "minProperties": 1, + "propertyNames": { + "oneOf": [ + { + "pattern": "^((iso)|(rfc))[0-9]+$" + }, + { + "enum": ["core", "validation", "ecma262", "perl5", "quote"] + } + ] + } + } + } }, + "additionalProperties": false + }, - "$defs": { - "test": { - "description": "A single test", + "$defs": { + "test": { + "description": "A single test", - "type": "object", - "required": [ "description", "data", "valid" ], - "properties": { - "description": { - "description": "The test description, briefly explaining which behavior it exercises", - "type": "string" - }, - "comment": { - "description": "Any additional comments about the test", - "type": "string" - }, - "data": { - "description": "The instance which should be validated against the schema in \"schema\"." - }, - "valid": { - "description": "Whether the validation process of this instance should consider the instance valid or not", - "type": "boolean" - } - }, - "additionalProperties": false + "type": "object", + "required": ["description", "data", "valid"], + "properties": { + "description": { + "description": "The test description, briefly explaining which behavior it exercises", + "type": "string" + }, + "comment": { + "description": "Any additional comments about the test", + "type": "string" + }, + "data": { + "description": "The instance which should be validated against the schema in \"schema\"." + }, + "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 } + } } diff --git a/tests/draft2020-12/enum.json b/tests/draft2020-12/enum.json index c8f35eac..9b87484b 100644 --- a/tests/draft2020-12/enum.json +++ b/tests/draft2020-12/enum.json @@ -1,358 +1,501 @@ [ - { - "description": "simple enum validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [1, 2, 3] - }, - "tests": [ - { - "description": "one of the enum is valid", - "data": 1, - "valid": true - }, - { - "description": "something else is invalid", - "data": 4, - "valid": false - } - ] + { + "description": "simple enum validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + 1, + 2, + 3 + ] }, - { - "description": "heterogeneous enum validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [6, "foo", [], true, {"foo": 12}] - }, - "tests": [ - { - "description": "one of the enum is valid", - "data": [], - "valid": true - }, - { - "description": "something else is invalid", - "data": null, - "valid": false - }, - { - "description": "objects are deep compared", - "data": {"foo": false}, - "valid": false - }, - { - "description": "valid object matches", - "data": {"foo": 12}, - "valid": true - }, - { - "description": "extra properties in object is invalid", - "data": {"foo": 12, "boo": 42}, - "valid": false - } - ] + "tests": [ + { + "description": "one of the enum is valid", + "data": 1, + "valid": true, + "id": "bc16cb75d14903a732326a24d1416757" + }, + { + "description": "something else is invalid", + "data": 4, + "valid": false, + "id": "2ea4a168ef00d32d444b3d49dc5a617d" + } + ] + }, + { + "description": "heterogeneous enum validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + 6, + "foo", + [], + true, + { + "foo": 12 + } + ] }, - { - "description": "heterogeneous enum-with-null validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [6, null] + "tests": [ + { + "description": "one of the enum is valid", + "data": [], + "valid": true, + "id": "e31a02b023906271a7f40576a03df0de" + }, + { + "description": "something else is invalid", + "data": null, + "valid": false, + "id": "b47e0170004d316b00a5151b0d2b566c" + }, + { + "description": "objects are deep compared", + "data": { + "foo": false }, - "tests": [ - { - "description": "null is valid", - "data": null, - "valid": true - }, - { - "description": "number is valid", - "data": 6, - "valid": true - }, - { - "description": "something else is invalid", - "data": "test", - "valid": false - } - ] - }, - { - "description": "enums in properties", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type":"object", - "properties": { - "foo": {"enum":["foo"]}, - "bar": {"enum":["bar"]} - }, - "required": ["bar"] + "valid": false, + "id": "7285822674e57f31de9c7280fa9d8900" + }, + { + "description": "valid object matches", + "data": { + "foo": 12 }, - "tests": [ - { - "description": "both properties are valid", - "data": {"foo":"foo", "bar":"bar"}, - "valid": true - }, - { - "description": "wrong foo value", - "data": {"foo":"foot", "bar":"bar"}, - "valid": false - }, - { - "description": "wrong bar value", - "data": {"foo":"foo", "bar":"bart"}, - "valid": false - }, - { - "description": "missing optional property is valid", - "data": {"bar":"bar"}, - "valid": true - }, - { - "description": "missing required property is invalid", - "data": {"foo":"foo"}, - "valid": false - }, - { - "description": "missing all properties is invalid", - "data": {}, - "valid": false - } - ] - }, - { - "description": "enum with escaped characters", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": ["foo\nbar", "foo\rbar"] + "valid": true, + "id": "27ffbacf5774ff2354458a6954ed1591" + }, + { + "description": "extra properties in object is invalid", + "data": { + "foo": 12, + "boo": 42 }, - "tests": [ - { - "description": "member 1 is valid", - "data": "foo\nbar", - "valid": true - }, - { - "description": "member 2 is valid", - "data": "foo\rbar", - "valid": true - }, - { - "description": "another string is invalid", - "data": "abc", - "valid": false - } - ] + "valid": false, + "id": "e220c92cdef194c74c49f9b7eb86b211" + } + ] + }, + { + "description": "heterogeneous enum-with-null validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + 6, + null + ] }, - { - "description": "enum with false does not match 0", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [false] + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true, + "id": "326f454f3db2a5fb76d797b43c812285" + }, + { + "description": "number is valid", + "data": 6, + "valid": true, + "id": "9884c0daef3095b4ff88c309bc87a620" + }, + { + "description": "something else is invalid", + "data": "test", + "valid": false, + "id": "4032c1754a28b94e914f8dfea1e12a26" + } + ] + }, + { + "description": "enums in properties", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { + "enum": [ + "foo" + ] }, - "tests": [ - { - "description": "false is valid", - "data": false, - "valid": true - }, - { - "description": "integer zero is invalid", - "data": 0, - "valid": false - }, - { - "description": "float zero is invalid", - "data": 0.0, - "valid": false - } - ] + "bar": { + "enum": [ + "bar" + ] + } + }, + "required": [ + "bar" + ] }, - { - "description": "enum with [false] does not match [0]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [[false]] + "tests": [ + { + "description": "both properties are valid", + "data": { + "foo": "foo", + "bar": "bar" }, - "tests": [ - { - "description": "[false] is valid", - "data": [false], - "valid": true - }, - { - "description": "[0] is invalid", - "data": [0], - "valid": false - }, - { - "description": "[0.0] is invalid", - "data": [0.0], - "valid": false - } - ] - }, - { - "description": "enum with true does not match 1", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [true] + "valid": true, + "id": "dba127f9663272184ec3ea3072676b4d" + }, + { + "description": "wrong foo value", + "data": { + "foo": "foot", + "bar": "bar" }, - "tests": [ - { - "description": "true is valid", - "data": true, - "valid": true - }, - { - "description": "integer one is invalid", - "data": 1, - "valid": false - }, - { - "description": "float one is invalid", - "data": 1.0, - "valid": false - } - ] - }, - { - "description": "enum with [true] does not match [1]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [[true]] + "valid": false, + "id": "0f0cca9923128d29922561fe7d92a436" + }, + { + "description": "wrong bar value", + "data": { + "foo": "foo", + "bar": "bart" }, - "tests": [ - { - "description": "[true] is valid", - "data": [true], - "valid": true - }, - { - "description": "[1] is invalid", - "data": [1], - "valid": false - }, - { - "description": "[1.0] is invalid", - "data": [1.0], - "valid": false - } - ] - }, - { - "description": "enum with 0 does not match false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [0] + "valid": false, + "id": "d32cef9094b8a87100a57cee099310d4" + }, + { + "description": "missing optional property is valid", + "data": { + "bar": "bar" }, - "tests": [ - { - "description": "false is invalid", - "data": false, - "valid": false - }, - { - "description": "integer zero is valid", - "data": 0, - "valid": true - }, - { - "description": "float zero is valid", - "data": 0.0, - "valid": true - } - ] - }, - { - "description": "enum with [0] does not match [false]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [[0]] + "valid": true, + "id": "23a7c86ef0b1e3cac9ebb6175de888d0" + }, + { + "description": "missing required property is invalid", + "data": { + "foo": "foo" }, - "tests": [ - { - "description": "[false] is invalid", - "data": [false], - "valid": false - }, - { - "description": "[0] is valid", - "data": [0], - "valid": true - }, - { - "description": "[0.0] is valid", - "data": [0.0], - "valid": true - } + "valid": false, + "id": "61ac5943d52ba688c77e26a1a7b5d174" + }, + { + "description": "missing all properties is invalid", + "data": {}, + "valid": false, + "id": "8b7be28a144634955b915ad780f4f2a5" + } + ] + }, + { + "description": "enum with escaped characters", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + "foo\nbar", + "foo\rbar" + ] + }, + "tests": [ + { + "description": "member 1 is valid", + "data": "foo\nbar", + "valid": true, + "id": "056ae2b9aad469dd32a63b1c97716c6e" + }, + { + "description": "member 2 is valid", + "data": "foo\rbar", + "valid": true, + "id": "ade764a27bb0fed294cefb1b6b275cd5" + }, + { + "description": "another string is invalid", + "data": "abc", + "valid": false, + "id": "1faca62e488e50dc47de0b3a26751477" + } + ] + }, + { + "description": "enum with false does not match 0", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + false + ] + }, + "tests": [ + { + "description": "false is valid", + "data": false, + "valid": true, + "id": "4e5b4da53732e4fdeaa5ed74127363bc" + }, + { + "description": "integer zero is invalid", + "data": 0, + "valid": false, + "id": "24f12f5bf7ae6be32a9634be17835176" + }, + { + "description": "float zero is invalid", + "data": 0, + "valid": false, + "id": "24f12f5bf7ae6be32a9634be17835176" + } + ] + }, + { + "description": "enum with [false] does not match [0]", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + [ + false ] + ] }, - { - "description": "enum with 1 does not match true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [1] - }, - "tests": [ - { - "description": "true is invalid", - "data": true, - "valid": false - }, - { - "description": "integer one is valid", - "data": 1, - "valid": true - }, - { - "description": "float one is valid", - "data": 1.0, - "valid": true - } + "tests": [ + { + "description": "[false] is valid", + "data": [ + false + ], + "valid": true, + "id": "9fe1fb5471d006782fe7ead4aa9909fa" + }, + { + "description": "[0] is invalid", + "data": [ + 0 + ], + "valid": false, + "id": "8fc4591c8c9ddf9bd41a234d5fa24521" + }, + { + "description": "[0.0] is invalid", + "data": [ + 0 + ], + "valid": false, + "id": "8fc4591c8c9ddf9bd41a234d5fa24521" + } + ] + }, + { + "description": "enum with true does not match 1", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + true + ] + }, + "tests": [ + { + "description": "true is valid", + "data": true, + "valid": true, + "id": "71738e4d71680e268bbee31fc43a4932" + }, + { + "description": "integer one is invalid", + "data": 1, + "valid": false, + "id": "e8901b103bcc1340391efd3e02420500" + }, + { + "description": "float one is invalid", + "data": 1, + "valid": false, + "id": "e8901b103bcc1340391efd3e02420500" + } + ] + }, + { + "description": "enum with [true] does not match [1]", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + [ + true ] + ] }, - { - "description": "enum with [1] does not match [true]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [[1]] - }, - "tests": [ - { - "description": "[true] is invalid", - "data": [true], - "valid": false - }, - { - "description": "[1] is valid", - "data": [1], - "valid": true - }, - { - "description": "[1.0] is valid", - "data": [1.0], - "valid": true - } + "tests": [ + { + "description": "[true] is valid", + "data": [ + true + ], + "valid": true, + "id": "44049e91dee93b7dafd9cdbc0156a604" + }, + { + "description": "[1] is invalid", + "data": [ + 1 + ], + "valid": false, + "id": "f8b1b069fd03b04f07f5b70786cb916c" + }, + { + "description": "[1.0] is invalid", + "data": [ + 1 + ], + "valid": false, + "id": "f8b1b069fd03b04f07f5b70786cb916c" + } + ] + }, + { + "description": "enum with 0 does not match false", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + 0 + ] + }, + "tests": [ + { + "description": "false is invalid", + "data": false, + "valid": false, + "id": "5cedc4948cc5d6744fa620039487ac15" + }, + { + "description": "integer zero is valid", + "data": 0, + "valid": true, + "id": "133e04205807df77ac09e730c237aba4" + }, + { + "description": "float zero is valid", + "data": 0, + "valid": true, + "id": "133e04205807df77ac09e730c237aba4" + } + ] + }, + { + "description": "enum with [0] does not match [false]", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + [ + 0 ] + ] }, - { - "description": "nul characters in strings", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ "hello\u0000there" ] - }, - "tests": [ - { - "description": "match string with nul", - "data": "hello\u0000there", - "valid": true - }, - { - "description": "do not match string lacking nul", - "data": "hellothere", - "valid": false - } + "tests": [ + { + "description": "[false] is invalid", + "data": [ + false + ], + "valid": false, + "id": "e6da8d988a7c265913e24eaed91af3de" + }, + { + "description": "[0] is valid", + "data": [ + 0 + ], + "valid": true, + "id": "10ba58a9539f8558892d28d2f5e3493d" + }, + { + "description": "[0.0] is valid", + "data": [ + 0 + ], + "valid": true, + "id": "10ba58a9539f8558892d28d2f5e3493d" + } + ] + }, + { + "description": "enum with 1 does not match true", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + 1 + ] + }, + "tests": [ + { + "description": "true is invalid", + "data": true, + "valid": false, + "id": "a8356b24823e955bc3895c25b9e442eb" + }, + { + "description": "integer one is valid", + "data": 1, + "valid": true, + "id": "603d6607a05072c21bcbff4578494e22" + }, + { + "description": "float one is valid", + "data": 1, + "valid": true, + "id": "603d6607a05072c21bcbff4578494e22" + } + ] + }, + { + "description": "enum with [1] does not match [true]", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + [ + 1 ] - } + ] + }, + "tests": [ + { + "description": "[true] is invalid", + "data": [ + true + ], + "valid": false, + "id": "732eec1f5b7fdf6d24c40d77072628e3" + }, + { + "description": "[1] is valid", + "data": [ + 1 + ], + "valid": true, + "id": "8064c0569b7c7a17631d0dc802090303" + }, + { + "description": "[1.0] is valid", + "data": [ + 1 + ], + "valid": true, + "id": "8064c0569b7c7a17631d0dc802090303" + } + ] + }, + { + "description": "nul characters in strings", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + "hello\u0000there" + ] + }, + "tests": [ + { + "description": "match string with nul", + "data": "hello\u0000there", + "valid": true, + "id": "d94ca2719908ab9c789d7c71e4cc93e4" + }, + { + "description": "do not match string lacking nul", + "data": "hellothere", + "valid": false, + "id": "a2c7a2aa3202a473dc03b5a8fcc070e9" + } + ] + } ]