UNPKG

alterschema

Version:

Convert between schema specifications

224 lines (202 loc) 9.56 kB
const tap = require('tap') const _ = require('lodash') const fs = require('fs') const os = require('os') const childProcess = require('child_process') const path = require('path') const walker = require('./walker') const builtin = require('./builtin') const jsonschema = require('./jsonschema') const alterschema = require('./index') const packageJSON = require('../../package.json') const METASCHEMAS = _.invert(require('../../metaschemas.json')) const JSON_SCHEMA_TEST_SUITE = path.resolve(__dirname, '..', '..', 'vendor', 'json-schema-test-suite') const TESTS_BASE_DIRECTORY = path.resolve(JSON_SCHEMA_TEST_SUITE, 'tests') const REMOTES_BASE_DIRECTORY = path.resolve(JSON_SCHEMA_TEST_SUITE, 'remotes') const recursiveReadDirectory = (directory) => { return fs.readdirSync(directory).reduce((accumulator, basename) => { const fullPath = path.resolve(directory, basename) return accumulator.concat(fs.statSync(fullPath).isDirectory() ? recursiveReadDirectory(fullPath) : fullPath) }, []) } for (const from of Object.keys(builtin.jsonschema)) { for (const to of Object.keys(builtin.jsonschema[from])) { const tests = require(`../../test/rules/jsonschema-${from}-to-${to}.json`) for (const testCase of tests) { tap.test(`${from} => ${to}: ${testCase.name}`, async (test) => { const result = await alterschema(testCase.schema, from, to) test.strictSame(result, testCase.expected) test.end() }) } } } for (const walkerName of fs.readdirSync(path.resolve(__dirname, '..', '..', 'test', 'walkers')).map((name) => { return path.basename(name, path.extname(name)) })) { for (const testCase of require(`../../test/walkers/${walkerName}.json`)) { tap.test(`${walkerName}: ${testCase.name}`, (test) => { const result = walker(walkerName, testCase.schema, []) test.strictSame(result, testCase.trail) test.end() }) } } for (const name of fs.readdirSync(path.resolve(__dirname, '..', '..', 'test', 'rules'))) { const rulesetPath = path.resolve(__dirname, '..', '..', 'rules', name) if (!fs.existsSync(rulesetPath)) { continue } for (const rule of require(rulesetPath).rules) { tap.test(`$id: (${name}) ${rule.condition.$id}`, (test) => { const basename = path.basename(name, path.extname(name)) const prefix = `https://github.com/sourcemeta/alterschema/rules/${basename}/` test.ok(rule.condition.$id.startsWith(prefix), `Must start with ${prefix}`) test.end() }) } } // TODO: Reduce this blacklist to a minimum const BLACKLIST = [ // The JSON Schema implementation used by this module cannot // ignore keywords in locations that are not subschemas. // See https://github.com/hyperjump-io/json-schema-validator/blob/7b352e75b2d2e37b54e854b2289ec137507bb174/lib/json-schema-test-suite.spec.ts#L32-L36 'draft4|id|id inside an enum is not a real identifier|exact match to enum, and type matches', 'draft4|id|id inside an enum is not a real identifier|match $ref to id', 'draft4|id|id inside an enum is not a real identifier|no match on enum or $ref to id', 'draft6|id|id inside an enum is not a real identifier|exact match to enum, and type matches', 'draft6|id|id inside an enum is not a real identifier|match $ref to id', 'draft6|id|id inside an enum is not a real identifier|no match on enum or $ref to id', 'draft7|id|id inside an enum is not a real identifier|exact match to enum, and type matches', 'draft7|id|id inside an enum is not a real identifier|match $ref to id', 'draft7|id|id inside an enum is not a real identifier|no match on enum or $ref to id', '2019-09|id|$id inside an enum is not a real identifier|exact match to enum, and type matches', '2019-09|id|$id inside an enum is not a real identifier|match $ref to $id', '2019-09|id|$id inside an enum is not a real identifier|no match on enum or $ref to $id', 'draft6|id|non-schema object containing a plain-name $id property|skip traversing definition for a valid result', 'draft6|id|non-schema object containing a plain-name $id property|const at const_not_anchor does not match', 'draft7|id|non-schema object containing a plain-name $id property|skip traversing definition for a valid result', 'draft7|id|non-schema object containing a plain-name $id property|const at const_not_anchor does not match', 'draft6|unknownKeyword|$id inside an unknown keyword is not a real identifier|type matches second anyOf, which has a real schema in it', 'draft6|unknownKeyword|$id inside an unknown keyword is not a real identifier|type matches non-schema in third anyOf', 'draft7|unknownKeyword|$id inside an unknown keyword is not a real identifier|type matches second anyOf, which has a real schema in it', 'draft7|unknownKeyword|$id inside an unknown keyword is not a real identifier|type matches non-schema in third anyOf', '2019-09|unknownKeyword|$id inside an unknown keyword is not a real identifier|type matches second anyOf, which has a real schema in it', '2019-09|unknownKeyword|$id inside an unknown keyword is not a real identifier|type matches non-schema in third anyOf', '2019-09|anchor|$anchor inside an enum is not a real identifier|exact match to enum, and type matches', '2019-09|anchor|$anchor inside an enum is not a real identifier|in implementations that strip $anchor, this may match either $def', '2019-09|anchor|$anchor inside an enum is not a real identifier|match $ref to $anchor', // TODO: Make these pass 'ref', 'refRemote' ] for (const from of Object.keys(builtin.jsonschema)) { // TODO: Support running draft3 tests if (from === 'draft3') { continue } const testId = from === '2020-12' || from === '2019-09' ? `draft${from}` : from const testsPath = path.resolve(TESTS_BASE_DIRECTORY, testId) for (const remote of recursiveReadDirectory(REMOTES_BASE_DIRECTORY)) { const relativePath = path.relative(REMOTES_BASE_DIRECTORY, remote) const schema = require(remote) const schemaId = path.posix.join('http://localhost:1234', relativePath) const dialect = METASCHEMAS[from] try { jsonschema.implementation.add(schema, schemaId, dialect) } catch (error) { console.error(`Cannot add remote ${relativePath} for ${dialect}`) } } for (const testPath of recursiveReadDirectory(testsPath)) { // We don't consider optional suites if (path.basename(path.dirname(testPath)) === 'optional' || path.basename(path.dirname(path.dirname(testPath))) === 'optional') { continue } const suiteName = path.basename(testPath, path.extname(testPath)) if (BLACKLIST.includes(suiteName)) { continue } for (const testCase of require(testPath)) { for (const instance of testCase.tests) { if (BLACKLIST.includes(`${from}|${suiteName}|${testCase.description}|${instance.description}`)) { continue } const index = testCase.tests.indexOf(instance) for (const to of Object.keys(builtin.jsonschema[from])) { tap.test(`${suiteName} (${from} -> ${to}) ${testCase.description} #${index}`, async (test) => { // We need at least an arbitrary id to make @hyperjump/json-schema work const id = `https://alterschema.sourcemeta.com/${_.kebabCase(testCase.description)}/${index}` if (from === 'draft4') { testCase.schema.id = testCase.schema.id || id } else { testCase.schema.$id = testCase.schema.$id || id } const metaschema = METASCHEMAS[from] test.ok(metaschema) if (typeof testCase.schema === 'boolean') { test.equal(testCase.schema, instance.valid) const schema = await alterschema(testCase.schema, from, to) test.equal(schema, testCase.schema) } else { testCase.schema.$schema = testCase.schema.$schema || metaschema const beforeResult = await jsonschema.matches(testCase.schema, instance.data) test.equal(beforeResult, instance.valid) const schema = await alterschema(testCase.schema, from, to) const afterResult = await jsonschema.matches(schema, instance.data) test.equal(afterResult, instance.valid) } test.end() }) } } } } } tap.test('(CLI) draft4 => 2020-12', (test) => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), packageJSON.name)) const schema = path.join(tmp, 'schema.json') fs.writeFileSync(schema, JSON.stringify({ id: 'http://example.com/schema', $schema: 'http://json-schema.org/draft-04/schema#', dependencies: { foo: ['bar'] }, properties: { foo: { enum: ['single-value'] }, bar: { type: 'number', minimum: 5, exclusiveMinimum: true } } }, null, 2)) const cli = path.resolve(__dirname, 'cli.js') const result = childProcess.spawnSync('node', [cli, '--from', 'draft4', '--to', '2020-12', schema]) fs.rmdirSync(tmp, { recursive: true }) test.strictSame(JSON.parse(result.stdout.toString()), { $id: 'http://example.com/schema', $schema: 'https://json-schema.org/draft/2020-12/schema', dependentRequired: { foo: ['bar'] }, properties: { foo: { const: 'single-value' }, bar: { type: 'number', exclusiveMinimum: 5 } } }) test.is(result.stderr.toString(), '') test.is(result.status, 0) test.end() })