Skip to content

Commit

Permalink
feat: multi-entrypoint compilation
Browse files Browse the repository at this point in the history
Adds a new `{ multi : true }` option to both validator() and parser(),
which allows comiling multiple schemas at once into a single module.

Closes: #140
  • Loading branch information
ChALkeR committed Oct 10, 2022
1 parent c9c0b34 commit 1272774
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 7 deletions.
27 changes: 20 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,38 @@ const jsonCheckWithErrors = (validate) =>
const jsonCheckWithoutErrors = (validate) => (data) =>
deepEqual(data, JSON.parse(JSON.stringify(data))) && validate(data)

const validator = (schema, rawOpts = {}) => {
const { parse = false, jsonCheck = false, isJSON = false, schemas = [], ...opts } = rawOpts
const validator = (
schema,
{ parse = false, multi = false, jsonCheck = false, isJSON = false, schemas = [], ...opts } = {}
) => {
if (jsonCheck && isJSON) throw new Error('Can not specify both isJSON and jsonCheck options')
if (parse && (jsonCheck || isJSON))
throw new Error('jsonCheck and isJSON options are not applicable in parser mode')
const mode = parse ? 'strong' : 'default' // strong mode is default in parser, can be overriden
const willJSON = isJSON || jsonCheck || parse
const options = { mode, ...opts, schemas: buildSchemas(schemas, [schema]), isJSON: willJSON }
const { scope, refs } = compile([schema], options) // only a single ref
const arg = multi ? schema : [schema]
const options = { mode, ...opts, schemas: buildSchemas(schemas, arg), isJSON: willJSON }
const { scope, refs } = compile(arg, options) // only a single ref
if (opts.dryRun) return
const fun = genfun()
if (parse) {
scope.parseWrap = opts.includeErrors ? parseWithErrors : parseWithoutErrors
fun.write('parseWrap(%s)', refs[0])
} else if (jsonCheck) {
scope.deepEqual = deepEqual
scope.jsonCheckWrap = opts.includeErrors ? jsonCheckWithErrors : jsonCheckWithoutErrors
fun.write('jsonCheckWrap(%s)', refs[0])
} else fun.write('%s', refs[0])
}
if (multi) {
fun.write('[')
for (const ref of refs.slice(0, -1)) fun.write('%s,', ref)
if (refs.length > 0) fun.write('%s', refs[refs.length - 1])
fun.write(']')
if (parse) fun.write('.map(parseWrap)')
else if (jsonCheck) fun.write('.map(jsonCheckWrap)')
} else {
if (parse) fun.write('parseWrap(%s)', refs[0])
else if (jsonCheck) fun.write('jsonCheckWrap(%s)', refs[0])
else fun.write('%s', refs[0])
}
const validate = fun.makeFunction(scope)
validate.toModule = ({ semi = true } = {}) => fun.makeModule(scope) + (semi ? ';' : '')
validate.toJSON = () => schema
Expand Down
185 changes: 185 additions & 0 deletions test/multi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
'use strict'

const tape = require('tape')
const { validator, parser } = require('../')

const $schema = 'https://json-schema.org/draft/2020-12/schema#'

const schemas1 = [{ $schema, type: 'number' }]
const schemas3 = [
{
$id: 'https://example.com/a',
type: 'object',
properties: {
value: { $ref: 'https://example.com/b' },
},
},
{
$id: 'https://example.com/b',
type: 'array',
items: { $ref: 'https://example.com/a' },
},
{ type: 'number' },
]

tape('multi, 0 schemas, validator', (t) => {
const validates = validator([], { multi: true })
t.strictEqual(validates.length, 0)
t.end()
})

tape('multi, 0 schemas, parser', (t) => {
const parses = parser([], { multi: true })
t.strictEqual(parses.length, 0)
t.end()
})

tape('multi, 1 schemas, validator', (t) => {
const validates = validator(schemas1, { multi: true })
t.strictEqual(validates.length, 1)

t.ok(validates[0](1))
t.strictEqual(validates[0].errors, undefined)
t.notOk(validates[0](''))
t.strictEqual(validates[0].errors, undefined)

t.end()
})

tape('multi, 1 schemas, validator, errors', (t) => {
const validates = validator(schemas1, { multi: true, includeErrors: true })
t.strictEqual(validates.length, 1)

t.ok(validates[0](1))
t.strictEqual(validates[0].errors, null)
t.notOk(validates[0](''))
t.deepEqual(validates[0].errors, [{ keywordLocation: '#/type', instanceLocation: '#' }])

t.end()
})

tape('multi, 1 schemas, parser', (t) => {
const parses = parser(schemas1, { multi: true })
t.strictEqual(parses.length, 1)

t.deepEqual(parses[0]('1'), { valid: true, value: 1 })
t.deepEqual(parses[0]('""'), { valid: false })
t.deepEqual(parses[0]('"'), { valid: false })

t.end()
})

tape('multi, 1 schemas, parser, errors', (t) => {
const parses = parser(schemas1, { multi: true, includeErrors: true })
t.strictEqual(parses.length, 1)

t.deepEqual(parses[0]('1'), { valid: true, value: 1 })
t.deepEqual(parses[0]('""'), {
valid: false,
error: 'JSON validation failed for type at #',
errors: [{ keywordLocation: '#/type', instanceLocation: '#' }],
})
t.deepEqual(parses[0]('"'), { valid: false, error: 'Unexpected end of JSON input' })

t.end()
})

tape('multi, 3 schemas, validator', (t) => {
const validates = validator(schemas3, { multi: true })
t.strictEqual(validates.length, 3)

t.ok(validates[0]({ value: [] }))
t.strictEqual(validates[2].errors, undefined)
t.notOk(validates[0]({ value: 10 }))
t.strictEqual(validates[2].errors, undefined)

t.ok(validates[1]([{ value: [] }]))
t.strictEqual(validates[2].errors, undefined)
t.notOk(validates[1]([{ value: 10 }]))
t.strictEqual(validates[2].errors, undefined)

t.ok(validates[2](1))
t.strictEqual(validates[2].errors, undefined)
t.notOk(validates[2](''))
t.strictEqual(validates[2].errors, undefined)

t.end()
})

tape('multi, 3 schemas, validator, errors', (t) => {
const validates = validator(schemas3, { multi: true, includeErrors: true })
t.strictEqual(validates.length, 3)

t.ok(validates[0]({ value: [] }))
t.strictEqual(validates[0].errors, null)
t.notOk(validates[0]({ value: 10 }))
t.deepEqual(validates[0].errors, [
{ keywordLocation: '#/properties/value/$ref/type', instanceLocation: '#/value' },
])

t.ok(validates[1]([{ value: [] }]))
t.strictEqual(validates[1].errors, null)
t.notOk(validates[1]([{ value: 10 }]))
t.deepEqual(validates[1].errors, [
{ keywordLocation: '#/items/$ref/properties/value/$ref/type', instanceLocation: '#/0/value' },
])

t.ok(validates[2](1))
t.strictEqual(validates[2].errors, null)
t.notOk(validates[2](''))
t.deepEqual(validates[2].errors, [{ keywordLocation: '#/type', instanceLocation: '#' }])

t.end()
})

tape('multi, 3 schemas, parser', (t) => {
const parses = parser(schemas3, { multi: true, mode: 'default' })
t.strictEqual(parses.length, 3)

t.deepEqual(parses[0]('{"value":[]}'), { valid: true, value: { value: [] } })
t.deepEqual(parses[0]('{"value":10}'), { valid: false })
t.deepEqual(parses[0]('"'), { valid: false })

t.deepEqual(parses[1]('[{"value":[]}]'), { valid: true, value: [{ value: [] }] })
t.deepEqual(parses[1]('[{"value":10}]'), { valid: false })
t.deepEqual(parses[1]('"'), { valid: false })

t.deepEqual(parses[2]('1'), { valid: true, value: 1 })
t.deepEqual(parses[2]('""'), { valid: false })
t.deepEqual(parses[2]('"'), { valid: false })

t.end()
})

tape('multi, 3 schemas, parser, errors', (t) => {
const parses = parser(schemas3, { multi: true, mode: 'default', includeErrors: true })
t.strictEqual(parses.length, 3)

t.deepEqual(parses[0]('{"value":[]}'), { valid: true, value: { value: [] } })
t.deepEqual(parses[0]('{"value":10}'), {
valid: false,
error: 'JSON validation failed for type at #/value',
errors: [{ keywordLocation: '#/properties/value/$ref/type', instanceLocation: '#/value' }],
})
t.deepEqual(parses[0]('"'), { valid: false, error: 'Unexpected end of JSON input' })

t.deepEqual(parses[1]('[{"value":[]}]'), { valid: true, value: [{ value: [] }] })
t.deepEqual(parses[1]('[{"value":10}]'), {
valid: false,
error: 'JSON validation failed for type at #/0/value',
errors: [
{ keywordLocation: '#/items/$ref/properties/value/$ref/type', instanceLocation: '#/0/value' },
],
})
t.deepEqual(parses[1]('"'), { valid: false, error: 'Unexpected end of JSON input' })

t.deepEqual(parses[2]('1'), { valid: true, value: 1 })
t.deepEqual(parses[2]('""'), {
valid: false,
error: 'JSON validation failed for type at #',
errors: [{ keywordLocation: '#/type', instanceLocation: '#' }],
})
t.deepEqual(parses[2]('"'), { valid: false, error: 'Unexpected end of JSON input' })

t.end()
})

0 comments on commit 1272774

Please sign in to comment.