@exodus/schemasafe
Advanced tools
Comparing version 1.0.0-beta.2 to 1.0.0-beta.3
{ | ||
"name": "@exodus/schemasafe", | ||
"version": "1.0.0-beta.2", | ||
"version": "1.0.0-beta.3", | ||
"description": "JSON Safe Parser & Schema Validator", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -5,3 +5,3 @@ # `@exodus/schemasafe` | ||
Supports [draft-04/06/07](doc/Specification-support.md). | ||
Supports [draft-04/06/07/2019-09](doc/Specification-support.md). | ||
@@ -8,0 +8,0 @@ [![Node CI Status](https://github.com/ExodusMovement/schemasafe/workflows/Node%20CI/badge.svg)](https://github.com/ExodusMovement/schemasafe/actions) |
@@ -11,3 +11,3 @@ 'use strict' | ||
const { knownKeywords, schemaVersions, knownVocabularies } = require('./known-keywords') | ||
const { initTracing, andDelta, orDelta, applyDelta, isDynamic } = require('./tracing') | ||
const { initTracing, andDelta, orDelta, applyDelta, isDynamic, inProperties } = require('./tracing') | ||
@@ -35,6 +35,7 @@ const noopRegExps = new Set(['^[\\s\\S]*$', '^[\\S\\s]*$', '^[^]*$', '', '.*', '^', '$']) | ||
const evaluatedStatic = Symbol('evaluated') | ||
const evaluatedStatic = Symbol('evaluatedStatic') | ||
const optDynamic = Symbol('optDynamic') | ||
const rootMeta = new WeakMap() | ||
const compile = (schema, root, opts, scope, basePathRoot) => { | ||
const compileSchema = (schema, root, opts, scope, basePathRoot) => { | ||
const { | ||
@@ -46,3 +47,2 @@ mode = 'default', | ||
allErrors = false, | ||
reflectErrorsValue = false, | ||
dryRun = false, | ||
@@ -76,4 +76,3 @@ allowUnusedKeywords = opts.mode === 'lax', | ||
throw new Error('Strong mode forbids weakFormats and allowUnusedKeywords') | ||
if (!includeErrors && (allErrors || reflectErrorsValue)) | ||
throw new Error('allErrors and reflectErrorsValue are not available if includeErrors = false') | ||
if (!includeErrors && allErrors) throw new Error('allErrors requires includeErrors to be enabled') | ||
@@ -127,2 +126,3 @@ const { gensym, getref, genref, genformat } = scopeMethods(scope) | ||
if (allErrors) fun.write('let errorCount = 0') | ||
if (opts[optDynamic]) fun.write('validate.evaluatedDynamic = null') | ||
@@ -135,4 +135,5 @@ const helpers = jsHelpers(fun, scope, propvar, { unmodifiedPrototypes, isJSON }, noopRegExps) | ||
const basePathStack = basePathRoot ? [basePathRoot] : [] | ||
const visit = (errors, history, current, node, schemaPath) => { | ||
const visit = (errors, history, current, node, schemaPath, trace = {}) => { | ||
// e.g. top-level data and property names, OR already checked by present() in history, OR in keys and not undefined | ||
const isSub = history.length > 0 && history[history.length - 1].prop === current | ||
const queryCurrent = () => history.filter((h) => h.prop === current) | ||
@@ -145,3 +146,3 @@ const definitelyPresent = | ||
const error = ({ path = [], prop = current, source }) => { | ||
const error = ({ path = [], prop = current, source, suberr }) => { | ||
const schemaP = functions.toPointer([...schemaPath, ...path]) | ||
@@ -160,10 +161,3 @@ const dataP = includeErrors ? buildPath(prop) : null | ||
} else if (includeErrors === true && errors) { | ||
const errorJS = reflectErrorsValue | ||
? format( | ||
'{ keywordLocation: %j, instanceLocation: %s, value: %s }', | ||
schemaP, | ||
dataP, | ||
buildName(prop) | ||
) | ||
: format('{ keywordLocation: %j, instanceLocation: %s }', schemaP, dataP) | ||
const errorJS = format('{ keywordLocation: %j, instanceLocation: %s }', schemaP, dataP) | ||
if (allErrors) { | ||
@@ -177,2 +171,3 @@ fun.write('if (%s === null) %s = []', errors, errors) | ||
} | ||
if (suberr) mergeerror(suberr) // can only happen in allErrors | ||
if (allErrors) fun.write('errorCount++') | ||
@@ -193,3 +188,4 @@ else fun.write('return false') | ||
const enforceMinMax = (a, b) => laxMode(!(node[b] < node[a]), `Invalid ${a} / ${b} combination`) | ||
const enforceValidation = (msg) => enforce(!requireValidation, `[requireValidation] ${msg}`) | ||
const enforceValidation = (msg, suffix = 'must be specified') => | ||
enforce(!requireValidation, `[requireValidation] ${msg} ${suffix}`) | ||
const subPath = (...args) => [...schemaPath, ...args] | ||
@@ -204,4 +200,4 @@ | ||
// any is valid | ||
enforceValidation('schema = true is not allowed') | ||
return stat // nothing is evaluated for true | ||
enforceValidation('schema = true', 'is not allowed') | ||
return { stat } // nothing is evaluated for true | ||
} else if (definitelyPresent) { | ||
@@ -214,4 +210,4 @@ // node === false always fails in this case | ||
} | ||
evaluateDelta({ properties: [true], items: Infinity }) // everything is evaluated for false | ||
return stat | ||
evaluateDelta({ properties: [true], items: Infinity, type: [] }) // everything is evaluated for false | ||
return { stat } | ||
} | ||
@@ -224,4 +220,4 @@ | ||
if (Object.keys(node).length === 0) { | ||
enforceValidation('empty rules node encountered') | ||
return stat // nothing to validate here, basically the same as node === true | ||
enforceValidation('empty rules node', 'encountered') | ||
return { stat } // nothing to validate here, basically the same as node === true | ||
} | ||
@@ -234,17 +230,28 @@ | ||
enforce(ruleTypes.every((t) => schemaTypes.has(t)), 'Invalid type used in consume') | ||
enforce(ruleTypes.some((t) => schemaTypes.get(t)(node[prop])), 'Type not expected:', prop) | ||
enforce(ruleTypes.some((t) => schemaTypes.get(t)(node[prop])), 'Unexpected type for', prop) | ||
unused.delete(prop) | ||
} | ||
const get = (prop, ...ruleTypes) => { | ||
if (node[prop] !== undefined) consume(prop, ...ruleTypes) | ||
return node[prop] | ||
} | ||
const handle = (prop, ruleTypes, handler, errorArgs = {}) => { | ||
if (node[prop] === undefined) return false | ||
// opt-out on null is explicit in both places here, don't set default | ||
consume(prop, ...ruleTypes) | ||
if (handler !== null) { | ||
const condition = handler(node[prop]) | ||
if (condition !== null) errorIf(condition, { path: [prop], ...errorArgs }) | ||
} | ||
return true | ||
} | ||
const finish = () => { | ||
const finish = (local) => { | ||
if (!definitelyPresent) fun.write('}') // undefined check | ||
enforce(unused.size === 0 || allowUnusedKeywords, 'Unprocessed keywords:', [...unused]) | ||
return { stat, local } // return statically evaluated | ||
} | ||
if (node === root) { | ||
const $schema = node.$schema || $schemaDefault | ||
if (node.$schema) { | ||
if (typeof node.$schema !== 'string') throw new Error('Unexpected $schema') | ||
consume('$schema', 'string') | ||
} | ||
const $schema = get('$schema', 'string') || $schemaDefault | ||
if ($schema) { | ||
@@ -258,122 +265,121 @@ const version = $schema.replace(/^http:\/\//, 'https://').replace(/#$/, '') | ||
exclusiveRefs: schemaIsOlderThan('draft/2019-09'), | ||
booleanRequired: schemaIsOlderThan('draft-04'), | ||
}) | ||
} | ||
if (node.$vocabulary) { | ||
for (const [vocab, flag] of Object.entries(node.$vocabulary)) { | ||
handle('$vocabulary', ['object'], ($vocabulary) => { | ||
for (const [vocab, flag] of Object.entries($vocabulary)) { | ||
if (flag === false) continue | ||
enforce(flag === true && knownVocabularies.includes(vocab), 'Unknown vocabulary:', vocab) | ||
} | ||
consume('$vocabulary', 'object') | ||
} | ||
return null | ||
}) | ||
} | ||
if (node === schema && recursiveAnchor) consume('$recursiveAnchor', 'boolean') | ||
if (node === schema && recursiveAnchor) handle('$recursiveAnchor', ['boolean'], null) // already applied | ||
if (typeof node.description === 'string') consume('description', 'string') // unused, meta-only | ||
if (typeof node.title === 'string') consume('title', 'string') // unused, meta-only | ||
if (typeof node.$comment === 'string') consume('$comment', 'string') // unused, meta-only | ||
if (Array.isArray(node.examples)) consume('examples', 'array') // unused, meta-only | ||
handle('deprecated', ['boolean'], null) // unused, meta-only | ||
handle('description', ['string'], null) // unused, meta-only | ||
handle('title', ['string'], null) // unused, meta-only | ||
handle('$comment', ['string'], null) // unused, meta-only | ||
handle('examples', ['array'], null) // unused, meta-only | ||
// defining defs are allowed, those are validated on usage | ||
if (typeof node.$defs === 'object') { | ||
consume('$defs', 'object') | ||
} else if (typeof node.definitions === 'object') { | ||
consume('definitions', 'object') | ||
} | ||
handle('$defs', ['object'], null) || handle('definitions', ['object'], null) // defs are allowed, those are validated on usage | ||
const basePath = () => (basePathStack.length > 0 ? basePathStack[basePathStack.length - 1] : '') | ||
if (typeof node.$id === 'string') { | ||
basePathStack.push(joinPath(basePath(), node.$id)) | ||
consume('$id', 'string') | ||
} else if (typeof node.id === 'string') { | ||
basePathStack.push(joinPath(basePath(), node.id)) | ||
consume('id', 'string') | ||
const setId = ($id) => { | ||
basePathStack.push(joinPath(basePath(), $id)) | ||
return null | ||
} | ||
// $anchor is used only for ref resolution, on usage | ||
if (typeof node.$anchor === 'string') consume('$anchor', 'string') | ||
handle('$id', ['string'], setId) || handle('id', ['string'], setId) | ||
handle('$anchor', ['string'], null) // $anchor is used only for ref resolution, on usage | ||
const booleanRequired = getMeta().booleanRequired && typeof node.required === 'boolean' | ||
if (node.default !== undefined && !useDefaults) consume('default', 'jsonval') // unused in this case | ||
const defaultIsPresent = node.default !== undefined && useDefaults // will consume on use | ||
if (definitelyPresent) { | ||
if (defaultIsPresent) fail('Can not apply default value here (e.g. at root)') | ||
if (node.required === true || node.required === false) | ||
fail('Can not apply boolean required here (e.g. at root)') | ||
} else if (defaultIsPresent || booleanRequired) { | ||
if (node.default !== undefined && useDefaults) { | ||
if (definitelyPresent) fail('Can not apply default value here (e.g. at root)') | ||
fun.write('if (%s) {', safenot(present(current))) | ||
if (defaultIsPresent) { | ||
fun.write('%s = %j', name, node.default) | ||
consume('default', 'jsonval') | ||
} | ||
if (booleanRequired) { | ||
if (node.required === true) { | ||
if (!defaultIsPresent) error({ path: ['required'] }) | ||
consume('required', 'boolean') | ||
} else if (node.required === false) { | ||
consume('required', 'boolean') | ||
} | ||
} | ||
fun.write('%s = %j', name, get('default', 'jsonval')) | ||
fun.write('} else {') | ||
} else { | ||
fun.write('if (%s) {', present(current)) | ||
handle('default', ['jsonval'], null) // unused | ||
if (!definitelyPresent) fun.write('if (%s) {', present(current)) | ||
} | ||
// evaluated: declare dynamic | ||
const needUnevaluated = (rule) => | ||
opts[optDynamic] && (node[rule] || node[rule] === false || node === schema) | ||
const local = Object.freeze({ | ||
items: needUnevaluated('unevaluatedItems') ? gensym('evaluatedItems') : null, | ||
props: needUnevaluated('unevaluatedProperties') ? gensym('evaluatedProps') : null, | ||
}) | ||
if (local.items) fun.write('const %s = [0]', local.items) | ||
if (local.props) fun.write('const %s = [[], []]', local.props) | ||
const dyn = { items: local.items || trace.items, props: local.props || trace.props } | ||
const canSkipDynamic = () => | ||
(!dyn.items || stat.items === Infinity) && (!dyn.props || stat.properties.includes(true)) | ||
const evaluateDeltaDynamic = (delta) => { | ||
// Skips applying those that have already been proved statically | ||
if (dyn.items && delta.items > stat.items) fun.write('%s.push(%d)', dyn.items, delta.items) | ||
if (dyn.props) { | ||
const inStat = (properties, patterns) => inProperties(stat, { properties, patterns }) | ||
const properties = delta.properties.filter((x) => !inStat([x], [])) | ||
const patterns = delta.patterns.filter((x) => !inStat([], [x])) | ||
if (properties.includes(true)) { | ||
fun.write('%s[0].push(true)', dyn.props) | ||
} else { | ||
if (properties.length > 0) fun.write('%s[0].push(...%j)', dyn.props, properties) | ||
if (patterns.length > 0) fun.write('%s[1].push(...%s)', dyn.props, patterns) | ||
} | ||
} | ||
} | ||
const applyDynamicToDynamic = (target, items, props) => { | ||
if (isDynamic(stat).items && target.items && items) | ||
fun.write('%s.push(...%s)', target.items, items) | ||
if (isDynamic(stat).properties && target.props && props) { | ||
fun.write('%s[0].push(...%s[0])', target.props, props) | ||
fun.write('%s[1].push(...%s[1])', target.props, props) | ||
} | ||
} | ||
const applyRef = (n, errorArgs) => { | ||
// evaluated: propagate static from ref to current, skips cyclic. | ||
// Can do this before the call as the call is just a write | ||
const delta = (scope[n] && scope[n][evaluatedStatic]) || { unknown: true } // assume unknown if ref is cyclic | ||
evaluateDelta(delta) | ||
// Allow recursion to here only if $recursiveAnchor is true, else skip from deep recursion | ||
const recursive = recursiveAnchor ? format('recursive || validate') : format('recursive') | ||
if (includeErrors) { | ||
// Save and restore errors in case of recursion | ||
const res = gensym('res') | ||
const err = gensym('err') | ||
const suberr = gensym('suberr') | ||
fun.write('const %s = validate.errors', err) | ||
fun.write('const %s = %s(%s, %s)', res, n, name, recursive) | ||
fun.write('const %s = %s.errors', suberr, n) | ||
fun.write('validate.errors = %s', err) | ||
errorIf(safenot(res), { ...errorArgs, source: suberr }) | ||
} else { | ||
errorIf(format('!%s(%s, %s)', n, name, recursive), errorArgs) | ||
} | ||
// evaluated: propagate static from ref to current, skips cyclic | ||
if (scope[n] && scope[n][evaluatedStatic]) evaluateDelta(scope[n][evaluatedStatic]) | ||
else evaluateDelta({ unknown: true }) // assume unknown if ref is cyclic | ||
if (!includeErrors && canSkipDynamic()) return format('!%s(%s, %s)', n, name, recursive) // simple case | ||
const res = gensym('res') | ||
const err = gensym('err') // Save and restore errors in case of recursion (if needed) | ||
const suberr = gensym('suberr') | ||
if (includeErrors) fun.write('const %s = validate.errors', err) | ||
fun.write('const %s = %s(%s, %s)', res, n, name, recursive) | ||
if (includeErrors) fun.write('const %s = %s.errors', suberr, n) | ||
if (includeErrors) fun.write('validate.errors = %s', err) | ||
errorIf(safenot(res), { ...errorArgs, source: suberr }) | ||
// evaluated: propagate dynamic from ref to current | ||
fun.if(res, () => { | ||
const items = isDynamic(delta).items ? format('%s.evaluatedDynamic[0]', n) : null | ||
const props = isDynamic(delta).properties ? format('%s.evaluatedDynamic[1]', n) : null | ||
applyDynamicToDynamic(dyn, items, props) | ||
}) | ||
return null | ||
} | ||
if (node.$ref) { | ||
handle('$ref', ['string'], ($ref) => { | ||
const resolved = resolveReference(root, schemas, node.$ref, basePath()) | ||
const [sub, subRoot, path] = resolved[0] || [] | ||
if (sub || sub === false) { | ||
let n = getref(sub) | ||
if (!n) n = compile(sub, subRoot, opts, scope, path) | ||
applyRef(n, { path: ['$ref'] }) | ||
} else fail('failed to resolve $ref:', node.$ref) | ||
consume('$ref', 'string') | ||
if (getMeta().exclusiveRefs) { | ||
// ref overrides any sibling keywords for older schemas | ||
finish() | ||
return stat | ||
} | ||
if (!sub && sub !== false) fail('failed to resolve $ref:', node.$ref) | ||
const n = getref(sub) || compileSchema(sub, subRoot, opts, scope, path) | ||
return applyRef(n, { path: ['$ref'] }) | ||
}) | ||
if (node.$ref && getMeta().exclusiveRefs) { | ||
enforce(!opts[optDynamic], 'unevaluated* is supported only on draft2019-09 schemas and above') | ||
return finish() // ref overrides any sibling keywords for older schemas | ||
} | ||
if (node.$recursiveRef) { | ||
enforce(node.$recursiveRef === '#', 'Behavior of $recursiveRef is defined only for "#"') | ||
handle('$recursiveRef', ['string'], ($recursiveRef) => { | ||
enforce($recursiveRef === '#', 'Behavior of $recursiveRef is defined only for "#"') | ||
// Apply deep recursion from here only if $recursiveAnchor is true, else just run self | ||
const n = recursiveAnchor ? format('(recursive || validate)') : format('validate') | ||
applyRef(n, { path: ['$recursiveRef'] }) | ||
consume('$recursiveRef', 'string') | ||
} | ||
return applyRef(n, { path: ['$recursiveRef'] }) | ||
}) | ||
/* Preparation and methods, post-$ref validation will begin at the end of the function */ | ||
const hasSubValidation = | ||
node.$ref || ['allOf', 'anyOf', 'oneOf'].some((key) => Array.isArray(node[key])) | ||
const typeArray = | ||
node.type === undefined ? null : Array.isArray(node.type) ? node.type : [node.type] | ||
for (const t of typeArray || []) | ||
enforce(typeof t === 'string' && types.has(t), 'Unknown type:', t) | ||
// typeArray === null and stat.type === null means no type validation, which is required if we don't have const or enum | ||
if (!typeArray && !stat.type && node.const === undefined && !node.enum && !hasSubValidation) | ||
enforceValidation('type is required') | ||
// This is used for typechecks, null means * here | ||
@@ -407,9 +413,7 @@ const allIn = (arr, valid) => { | ||
const prev = allErrors && haveComplex ? gensym('prev') : null | ||
const prevWrap = (shouldWrap, writeBody) => { | ||
if (prev === null || !shouldWrap) writeBody() | ||
else fun.if(format('errorCount === %s', prev), writeBody) | ||
} | ||
const prevWrap = (shouldWrap, writeBody) => | ||
fun.if(shouldWrap && prev !== null ? format('errorCount === %s', prev) : true, writeBody) | ||
// Can not be used before undefined check above! The one performed by present() | ||
const rule = (...args) => visit(errors, [...history, { stat, prop: current }], ...args) | ||
const rule = (...args) => visit(errors, [...history, { stat, prop: current }], ...args).stat | ||
const subrule = (suberr, ...args) => { | ||
@@ -419,3 +423,3 @@ const sub = gensym('sub') | ||
if (allErrors) fun.write('let errorCount = 0') // scoped error counter | ||
const delta = visit(suberr, [...history, { stat, prop: current }], ...args) | ||
const { stat: delta } = visit(suberr, [...history, { stat, prop: current }], ...args) | ||
if (allErrors) { | ||
@@ -434,31 +438,30 @@ fun.write('return errorCount === 0') | ||
const mergeerror = (suberr) => { | ||
if (!suberr) return | ||
// suberror can be null e.g. on failed empty contains | ||
const args = [errors, suberr, errors, suberr, suberr, errors, suberr] | ||
fun.write('if (%s && %s) { %s.push(...%s) } else if (%s) %s = %s', ...args) | ||
if (suberr !== null) fun.write('if (%s) %s.push(...%s)', suberr, errors, suberr) | ||
} | ||
// Extracted single additional(Items/Properties) rules, for reuse with unevaluated(Items/Properties) | ||
const additionalItems = (limit, ruleValue, rulePath) => { | ||
if (ruleValue === false) { | ||
if (removeAdditional) { | ||
const additionalItems = (rulePath, limit) => { | ||
const handled = handle(rulePath, ['object', 'boolean'], (ruleValue) => { | ||
if (ruleValue === false) { | ||
if (!removeAdditional) return format('%s.length > %s', name, limit) | ||
fun.write('if (%s.length > %s) %s.length = %s', name, limit, name, limit) | ||
} else { | ||
errorIf(format('%s.length > %s', name, limit), { path: [rulePath] }) | ||
return null | ||
} | ||
} else if (ruleValue) { | ||
forArray(current, limit, (prop) => rule(prop, ruleValue, subPath(rulePath))) | ||
} | ||
consume(rulePath, 'object', 'boolean') | ||
evaluateDelta({ items: Infinity }) | ||
return null | ||
}) | ||
if (handled) evaluateDelta({ items: Infinity }) | ||
} | ||
const additionalProperties = (condition, ruleValue, rulePath) => { | ||
forObjectKeys(current, (sub, key) => { | ||
fun.if(condition(key), () => { | ||
if (ruleValue === false && removeAdditional) fun.write('delete %s[%s]', name, key) | ||
else rule(sub, ruleValue, subPath(rulePath)) | ||
const additionalProperties = (rulePath, condition) => { | ||
const handled = handle(rulePath, ['object', 'boolean'], (ruleValue) => { | ||
forObjectKeys(current, (sub, key) => { | ||
fun.if(condition(key), () => { | ||
if (ruleValue === false && removeAdditional) fun.write('delete %s[%s]', name, key) | ||
else rule(sub, ruleValue, subPath(rulePath)) | ||
}) | ||
}) | ||
return null | ||
}) | ||
consume(rulePath, 'object', 'boolean') | ||
evaluateDelta({ properties: [true] }) | ||
if (handled) evaluateDelta({ properties: [true] }) | ||
} | ||
@@ -474,66 +477,47 @@ const additionalCondition = (key, properties, patternProperties) => | ||
const checkNumbers = () => { | ||
const applyMinMax = (value, operator, errorArgs) => { | ||
enforce(Number.isFinite(value), 'Invalid minimum or maximum:', value) | ||
errorIf(format('!(%d %c %s)', value, operator, name), errorArgs) | ||
} | ||
const minMax = (value, operator) => format('!(%d %c %s)', value, operator, name) // don't remove negation, accounts for NaN | ||
if (Number.isFinite(node.exclusiveMinimum)) { | ||
applyMinMax(node.exclusiveMinimum, '<', { path: ['exclusiveMinimum'] }) | ||
consume('exclusiveMinimum', 'finite') | ||
} else if (node.minimum !== undefined) { | ||
applyMinMax(node.minimum, node.exclusiveMinimum ? '<' : '<=', { path: ['minimum'] }) | ||
consume('minimum', 'finite') | ||
if (typeof node.exclusiveMinimum === 'boolean') consume('exclusiveMinimum', 'boolean') | ||
handle('exclusiveMinimum', ['finite'], (min) => minMax(min, '<')) | ||
} else { | ||
handle('minimum', ['finite'], (min) => minMax(min, node.exclusiveMinimum ? '<' : '<=')) | ||
handle('exclusiveMinimum', ['boolean'], null) // handled above | ||
} | ||
if (Number.isFinite(node.exclusiveMaximum)) { | ||
applyMinMax(node.exclusiveMaximum, '>', { path: ['exclusiveMaximum'] }) | ||
handle('exclusiveMaximum', ['finite'], (max) => minMax(max, '>')) | ||
enforceMinMax('minimum', 'exclusiveMaximum') | ||
enforceMinMax('exclusiveMinimum', 'exclusiveMaximum') | ||
consume('exclusiveMaximum', 'finite') | ||
} else if (node.maximum !== undefined) { | ||
applyMinMax(node.maximum, node.exclusiveMaximum ? '>' : '>=', { path: ['maximum'] }) | ||
handle('maximum', ['finite'], (max) => minMax(max, node.exclusiveMaximum ? '>' : '>=')) | ||
handle('exclusiveMaximum', ['boolean'], null) // handled above | ||
enforceMinMax('minimum', 'maximum') | ||
enforceMinMax('exclusiveMinimum', 'maximum') | ||
consume('maximum', 'finite') | ||
if (typeof node.exclusiveMaximum === 'boolean') consume('exclusiveMaximum', 'boolean') | ||
} | ||
const multipleOf = node.multipleOf === undefined ? 'divisibleBy' : 'multipleOf' // draft3 support | ||
if (node[multipleOf] !== undefined) { | ||
const value = node[multipleOf] | ||
enforce(Number.isFinite(value) && value > 0, `Invalid ${multipleOf}:`, value) | ||
if (Number.isInteger(value)) { | ||
errorIf(format('%s %% %d !== 0', name, value), { path: ['isMultipleOf'] }) | ||
} else { | ||
scope.isMultipleOf = functions.isMultipleOf | ||
const [last, exp] = `${value}`.replace(/.*\./, '').split('e-') | ||
const e = last.length + (exp ? Number(exp) : 0) | ||
const args = [name, value, e, Math.round(value * Math.pow(10, e))] // precompute for performance | ||
errorIf(format('!isMultipleOf(%s, %d, 1e%d, %d)', ...args), { path: ['isMultipleOf'] }) | ||
} | ||
consume(multipleOf, 'finite') | ||
} | ||
handle(multipleOf, ['finite'], (value) => { | ||
enforce(value > 0, `Invalid ${multipleOf}:`, value) | ||
if (Number.isInteger(value)) return format('%s %% %d !== 0', name, value) | ||
scope.isMultipleOf = functions.isMultipleOf | ||
const [last, exp] = `${value}`.replace(/.*\./, '').split('e-') | ||
const e = last.length + (exp ? Number(exp) : 0) | ||
const args = [name, value, e, Math.round(value * Math.pow(10, e))] // precompute for performance | ||
return format('!isMultipleOf(%s, %d, 1e%d, %d)', ...args) | ||
}) | ||
} | ||
const checkStrings = () => { | ||
if (node.maxLength !== undefined) { | ||
enforce(Number.isFinite(node.maxLength), 'Invalid maxLength:', node.maxLength) | ||
handle('maxLength', ['natural'], (max) => { | ||
scope.stringLength = functions.stringLength | ||
const args = [name, node.maxLength, name, node.maxLength] | ||
errorIf(format('%s.length > %d && stringLength(%s) > %d', ...args), { path: ['maxLength'] }) | ||
consume('maxLength', 'natural') | ||
} | ||
if (node.minLength !== undefined) { | ||
enforce(Number.isFinite(node.minLength), 'Invalid minLength:', node.minLength) | ||
enforceMinMax('minLength', 'maxLength') | ||
return format('%s.length > %d && stringLength(%s) > %d', name, max, name, max) | ||
}) | ||
handle('minLength', ['natural'], (min) => { | ||
scope.stringLength = functions.stringLength | ||
const args = [name, node.minLength, name, node.minLength] | ||
errorIf(format('%s.length < %d || stringLength(%s) < %d', ...args), { path: ['minLength'] }) | ||
consume('minLength', 'natural') | ||
} | ||
return format('%s.length < %d || stringLength(%s) < %d', name, min, name, min) | ||
}) | ||
enforceMinMax('minLength', 'maxLength') | ||
prevWrap(true, () => { | ||
const checkFormat = (fmtname, target, path, formatsObj = fmts) => { | ||
const checkFormat = (fmtname, target, formatsObj = fmts) => { | ||
const known = typeof fmtname === 'string' && functions.hasOwn(formatsObj, fmtname) | ||
@@ -548,19 +532,19 @@ enforce(known, 'Unrecognized format used:', fmtname) | ||
if (functions.hasOwn(optFormats, fmtname)) enforceRegex(formatImpl.source) | ||
errorIf(format('!%s.test(%s)', n, target), { path: [path] }) | ||
} else { | ||
errorIf(format('!%s(%s)', n, target), { path: [path] }) | ||
return format('!%s.test(%s)', n, target) | ||
} | ||
return format('!%s(%s)', n, target) | ||
} | ||
if (node.format) { | ||
checkFormat(node.format, name, 'format') | ||
consume('format', 'string') | ||
} | ||
if (node.pattern) { | ||
enforceRegex(node.pattern) | ||
if (!noopRegExps.has(node.pattern)) | ||
errorIf(safenot(patternTest(node.pattern, name)), { path: ['pattern'] }) | ||
consume('pattern', 'string') | ||
} | ||
handle('format', ['string'], (value) => { | ||
evaluateDelta({ fullstring: true }) | ||
return checkFormat(value, name) | ||
}) | ||
handle('pattern', ['string'], (pattern) => { | ||
enforceRegex(pattern) | ||
evaluateDelta({ fullstring: true }) | ||
if (noopRegExps.has(pattern)) return null | ||
return safenot(patternTest(pattern, name)) | ||
}) | ||
enforce(node.contentSchema !== false, 'contentSchema cannot be set to false') | ||
@@ -572,3 +556,3 @@ if (node.contentEncoding || node.contentMediaType || node.contentSchema) { | ||
if (node.contentEncoding === 'base64') { | ||
checkFormat('base64', name, 'contentEncoding', formats.extra) | ||
errorIf(checkFormat('base64', name, formats.extra), { path: ['contentEncoding'] }) | ||
if (node.contentMediaType) { | ||
@@ -595,2 +579,3 @@ scope.deBase64 = functions.deBase64 | ||
consume('contentSchema', 'object', 'array') | ||
evaluateDelta({ fullstring: true }) | ||
} | ||
@@ -609,53 +594,31 @@ if (node.contentMediaType) { | ||
}) | ||
const stringValidated = node.format || node.pattern || node.contentSchema || hasSubValidation | ||
const stringWarning = 'pattern, format or contentSchema must be specified for strings' | ||
if (typeApplicable('string') && requireStringValidation && !stringValidated) | ||
fail(`[requireStringValidation] ${stringWarning}, use pattern: ^[\\s\\S]*$ to opt-out`) | ||
} | ||
const checkArrays = () => { | ||
if (node.maxItems !== undefined) { | ||
enforce(Number.isFinite(node.maxItems), 'Invalid maxItems:', node.maxItems) | ||
if (Array.isArray(node.items) && node.items.length > node.maxItems) | ||
fail(`Invalid maxItems: ${node.maxItems} is less than items array length`) | ||
errorIf(format('%s.length > %d', name, node.maxItems), { path: ['maxItems'] }) | ||
consume('maxItems', 'natural') | ||
} | ||
handle('maxItems', ['natural'], (max) => { | ||
if (Array.isArray(node.items) && node.items.length > max) | ||
fail(`Invalid maxItems: ${max} is less than items array length`) | ||
return format('%s.length > %d', name, max) | ||
}) | ||
handle('minItems', ['natural'], (min) => format('%s.length < %d', name, min)) // can be higher that .items length with additionalItems | ||
enforceMinMax('minItems', 'maxItems') | ||
if (node.minItems !== undefined) { | ||
enforce(Number.isFinite(node.minItems), 'Invalid minItems:', node.minItems) | ||
enforceMinMax('minItems', 'maxItems') | ||
// can be higher that .items length with additionalItems | ||
errorIf(format('%s.length < %d', name, node.minItems), { path: ['minItems'] }) | ||
consume('minItems', 'natural') | ||
} | ||
if (node.items || node.items === false) { | ||
if (Array.isArray(node.items)) { | ||
for (let p = 0; p < node.items.length; p++) | ||
rule(currPropImm(p), node.items[p], subPath(`${p}`)) | ||
evaluateDelta({ items: node.items.length }) | ||
handle('items', ['object', 'array', 'boolean'], (items) => { | ||
if (Array.isArray(items)) { | ||
for (let p = 0; p < items.length; p++) rule(currPropImm(p), items[p], subPath(`${p}`)) | ||
evaluateDelta({ items: items.length }) | ||
} else { | ||
forArray(current, format('0'), (prop) => rule(prop, node.items, subPath('items'))) | ||
stat.items = Infinity | ||
forArray(current, format('0'), (prop) => rule(prop, items, subPath('items'))) | ||
evaluateDelta({ items: Infinity }) | ||
} | ||
consume('items', 'object', 'array', 'boolean') | ||
} else if (typeApplicable('array') && !hasSubValidation) { | ||
enforceValidation('items rule must be specified') | ||
} | ||
return null | ||
}) | ||
if (!Array.isArray(node.items)) { | ||
// additionalItems is allowed, but ignored per some spec tests in this case! | ||
// We do nothing and let it throw except for in allowUnusedKeywords mode | ||
// As a result, this is not allowed by default, only in allowUnusedKeywords mode | ||
} else if (node.additionalItems || node.additionalItems === false) { | ||
additionalItems(format('%d', node.items.length), node.additionalItems, 'additionalItems') | ||
} else if (node.items.length === node.maxItems) { | ||
// No additional items are possible | ||
} else { | ||
enforceValidation('additionalItems rule must be specified for fixed arrays') | ||
} | ||
if (Array.isArray(node.items)) | ||
additionalItems('additionalItems', format('%d', node.items.length)) | ||
// Else additionalItems is allowed, but ignored per some spec tests! | ||
// We do nothing and let it throw except for in allowUnusedKeywords mode | ||
// As a result, omitting .items is not allowed by default, only in allowUnusedKeywords mode | ||
if (node.contains || node.contains === false) { | ||
handle('contains', ['object', 'boolean'], () => { | ||
const passes = gensym('passes') | ||
@@ -671,21 +634,10 @@ fun.write('let %s = 0', passes) | ||
if (Number.isFinite(node.minContains)) { | ||
const condition = format('%s < %d', passes, node.minContains) // fast, reusable | ||
errorIf(condition, { path: ['minContains'] }) | ||
consume('minContains', 'natural') | ||
fun.if(condition, () => mergeerror(suberr)) | ||
} else { | ||
const condition = format('%s < 1', passes) // fast, reusable | ||
errorIf(condition, { path: ['contains'] }) | ||
fun.if(condition, () => mergeerror(suberr)) | ||
} | ||
if (!handle('minContains', ['natural'], (mn) => format('%s < %d', passes, mn), { suberr })) | ||
errorIf(format('%s < 1', passes), { path: ['contains'], suberr }) | ||
if (Number.isFinite(node.maxContains)) { | ||
errorIf(format('%s > %d', passes, node.maxContains), { path: ['maxContains'] }) | ||
enforceMinMax('minContains', 'maxContains') | ||
consume('maxContains', 'natural') | ||
} | ||
handle('maxContains', ['natural'], (max) => format('%s > %d', passes, max)) | ||
enforceMinMax('minContains', 'maxContains') | ||
consume('contains', 'object', 'boolean') | ||
} | ||
return null | ||
}) | ||
@@ -705,3 +657,4 @@ const uniqueIsSimple = () => { | ||
prevWrap(true, () => { | ||
if (node.uniqueItems === true) { | ||
handle('uniqueItems', ['boolean'], (uniqueItems) => { | ||
if (uniqueItems === false) return null | ||
if (complexityChecks) | ||
@@ -711,7 +664,4 @@ enforce(uniqueIsSimple(), 'maxItems should be specified for non-primitive uniqueItems') | ||
scope.deepEqual = functions.deepEqual | ||
errorIf(format('!unique(%s)', name), { path: ['uniqueItems'] }) | ||
consume('uniqueItems', 'boolean') | ||
} else if (node.uniqueItems === false) { | ||
consume('uniqueItems', 'boolean') | ||
} | ||
return format('!unique(%s)', name) | ||
}) | ||
}) | ||
@@ -722,17 +672,8 @@ } | ||
const propertiesCount = format('Object.keys(%s).length', name) | ||
if (node.maxProperties !== undefined) { | ||
enforce(Number.isFinite(node.maxProperties), 'Invalid maxProperties:', node.maxProperties) | ||
errorIf(format('%s > %d', propertiesCount, node.maxProperties), { path: ['maxProperties'] }) | ||
consume('maxProperties', 'natural') | ||
} | ||
if (node.minProperties !== undefined) { | ||
enforce(Number.isFinite(node.minProperties), 'Invalid minProperties:', node.minProperties) | ||
enforceMinMax('minProperties', 'maxProperties') | ||
errorIf(format('%s < %d', propertiesCount, node.minProperties), { path: ['minProperties'] }) | ||
consume('minProperties', 'natural') | ||
} | ||
handle('maxProperties', ['natural'], (max) => format('%s > %d', propertiesCount, max)) | ||
handle('minProperties', ['natural'], (min) => format('%s < %d', propertiesCount, min)) | ||
enforceMinMax('minProperties', 'maxProperties') | ||
if (typeof node.propertyNames === 'object' || typeof node.propertyNames === 'boolean') { | ||
handle('propertyNames', ['object', 'boolean'], (names) => { | ||
forObjectKeys(current, (sub, key) => { | ||
const names = node.propertyNames | ||
const nameSchema = typeof names === 'object' ? { type: 'string', ...names } : names | ||
@@ -742,6 +683,4 @@ const nameprop = Object.freeze({ name: key, errorParent: sub, type: 'string' }) | ||
}) | ||
consume('propertyNames', 'object', 'boolean') | ||
} | ||
if (typeof node.additionalProperties === 'object' && typeof node.propertyNames !== 'object') | ||
enforceValidation('wild-card additionalProperties requires propertyNames') | ||
return null | ||
}) | ||
@@ -753,4 +692,4 @@ // if allErrors is false, we can skip present check for required properties validated before | ||
if (Array.isArray(node.required)) { | ||
for (const req of node.required) { | ||
handle('required', ['array'], (required) => { | ||
for (const req of required) { | ||
if (checked(req)) continue | ||
@@ -760,11 +699,10 @@ const prop = currPropImm(req) | ||
} | ||
evaluateDelta({ required: node.required }) | ||
consume('required', 'array') | ||
} | ||
evaluateDelta({ required }) | ||
return null | ||
}) | ||
for (const dependencies of ['dependencies', 'dependentRequired', 'dependentSchemas']) { | ||
if (node[dependencies]) { | ||
for (const key of Object.keys(node[dependencies])) { | ||
let deps = node[dependencies][key] | ||
if (typeof deps === 'string') deps = [deps] | ||
handle(dependencies, ['object'], (value) => { | ||
for (const key of Object.keys(value)) { | ||
const deps = typeof value[key] === 'string' ? [value[key]] : value[key] | ||
const item = currPropImm(key, checked(key)) | ||
@@ -788,33 +726,33 @@ if (Array.isArray(deps) && dependencies !== 'dependentSchemas') { | ||
const body = () => { | ||
const delta = rule(current, deps, subPath(dependencies, key)) | ||
const delta = rule(current, deps, subPath(dependencies, key), dyn) | ||
evaluateDelta(orDelta({}, delta)) | ||
evaluateDeltaDynamic(delta) | ||
} | ||
if (item.checked) body() | ||
else fun.if(present(item), body) | ||
fun.if(item.checked ? true : present(item), body) | ||
} else fail(`Unexpected ${dependencies} entry`) | ||
} | ||
consume(dependencies, 'object') | ||
} | ||
return null | ||
}) | ||
} | ||
if (typeof node.properties === 'object') { | ||
for (const p of Object.keys(node.properties)) | ||
rule(currPropImm(p, checked(p)), node.properties[p], subPath('properties', p)) | ||
evaluateDelta({ properties: Object.keys(node.properties || {}) }) | ||
consume('properties', 'object') | ||
} | ||
handle('properties', ['object'], (properties) => { | ||
for (const p of Object.keys(properties)) | ||
rule(currPropImm(p, checked(p)), properties[p], subPath('properties', p)) | ||
evaluateDelta({ properties: Object.keys(properties || {}) }) | ||
return null | ||
}) | ||
prevWrap(node.patternProperties, () => { | ||
if (node.patternProperties) { | ||
handle('patternProperties', ['object'], (patternProperties) => { | ||
forObjectKeys(current, (sub, key) => { | ||
for (const p of Object.keys(node.patternProperties)) { | ||
for (const p of Object.keys(patternProperties)) { | ||
enforceRegex(p, node.propertyNames || {}) | ||
fun.if(patternTest(p, key), () => { | ||
rule(sub, node.patternProperties[p], subPath('patternProperties', p)) | ||
rule(sub, patternProperties[p], subPath('patternProperties', p)) | ||
}) | ||
} | ||
}) | ||
evaluateDelta({ patterns: Object.keys(node.patternProperties || {}) }) | ||
consume('patternProperties', 'object') | ||
} | ||
evaluateDelta({ patterns: Object.keys(patternProperties || {}) }) | ||
return null | ||
}) | ||
if (node.additionalProperties || node.additionalProperties === false) { | ||
@@ -824,5 +762,3 @@ const properties = Object.keys(node.properties || {}) | ||
const condition = (key) => additionalCondition(key, properties, patternProperties) | ||
additionalProperties(condition, node.additionalProperties, 'additionalProperties') | ||
} else if (typeApplicable('object') && !hasSubValidation) { | ||
enforceValidation('additionalProperties rule must be specified') | ||
additionalProperties('additionalProperties', condition) | ||
} | ||
@@ -833,32 +769,21 @@ }) | ||
const checkConst = () => { | ||
if (node.const !== undefined) { | ||
errorIf(safenot(compare(name, node.const)), { path: ['const'] }) | ||
consume('const', 'jsonval') | ||
return true | ||
} else if (node.enum) { | ||
enforce(Array.isArray(node.enum), 'Invalid enum') | ||
const objects = node.enum.filter((value) => value && typeof value === 'object') | ||
const primitive = node.enum.filter((value) => !(value && typeof value === 'object')) | ||
const condition = safeor(...[...primitive, ...objects].map((value) => compare(name, value))) | ||
errorIf(safenot(condition), { path: ['enum'] }) | ||
consume('enum', 'array') | ||
return true | ||
} | ||
return false | ||
if (handle('const', ['jsonval'], (val) => safenot(compare(name, val)))) return true | ||
return handle('enum', ['array'], (vals) => { | ||
const objects = vals.filter((value) => value && typeof value === 'object') | ||
const primitive = vals.filter((value) => !(value && typeof value === 'object')) | ||
return safenot(safeor(...[...primitive, ...objects].map((value) => compare(name, value)))) | ||
}) | ||
} | ||
const checkGeneric = () => { | ||
if (node.not || node.not === false) { | ||
const { sub } = subrule(null, current, node.not, subPath('not')) | ||
errorIf(sub, { path: ['not'] }) | ||
consume('not', 'object', 'boolean') | ||
} | ||
handle('not', ['object', 'boolean'], (not) => subrule(null, current, not, subPath('not')).sub) | ||
const thenOrElse = node.then || node.then === false || node.else || node.else === false | ||
if ((node.if || node.if === false) && thenOrElse) { | ||
const { sub, delta: deltaIf } = subrule(null, current, node.if, subPath('if')) | ||
const { sub, delta: deltaIf } = subrule(null, current, node.if, subPath('if'), dyn) | ||
let deltaElse, deltaThen | ||
fun.write('if (%s) {', safenot(sub)) | ||
if (node.else || node.else === false) { | ||
deltaElse = rule(current, node.else, subPath('else')) | ||
deltaElse = rule(current, node.else, subPath('else'), dyn) | ||
evaluateDeltaDynamic(deltaElse) | ||
consume('else', 'object', 'boolean') | ||
@@ -868,3 +793,4 @@ } else deltaElse = {} | ||
fun.write('} else {') | ||
deltaThen = rule(current, node.then, subPath('then')) | ||
deltaThen = rule(current, node.then, subPath('then'), dyn) | ||
evaluateDeltaDynamic(andDelta(deltaIf, deltaThen)) | ||
consume('then', 'object', 'boolean') | ||
@@ -877,14 +803,23 @@ } else deltaThen = {} | ||
if (node.allOf !== undefined) { | ||
enforce(Array.isArray(node.allOf), 'Invalid allOf') | ||
for (const [key, sch] of Object.entries(node.allOf)) | ||
evaluateDelta(rule(current, sch, subPath('allOf', key))) | ||
consume('allOf', 'array') | ||
} | ||
handle('allOf', ['array'], (allOf) => { | ||
for (const [key, sch] of Object.entries(allOf)) | ||
evaluateDelta(rule(current, sch, subPath('allOf', key), dyn)) | ||
return null | ||
}) | ||
if (node.anyOf !== undefined) { | ||
enforce(Array.isArray(node.anyOf), 'Invalid anyOf') | ||
handle('anyOf', ['array'], (anyOf) => { | ||
const suberr = suberror() | ||
if (anyOf.length > 0 && !canSkipDynamic()) { | ||
// In this case, all have to be checked to gather evaluated properties | ||
const entries = Object.entries(anyOf).map(([key, sch]) => | ||
subrule(suberr, current, sch, subPath('anyOf', key), dyn) | ||
) | ||
evaluateDelta(entries.reduce((acc, cur) => orDelta(acc, cur.delta), {})) | ||
const condition = safenot(safeor(...entries.map(({ sub }) => sub))) | ||
errorIf(condition, { path: ['anyOf'], suberr }) | ||
for (const { delta, sub } of entries) fun.if(sub, () => evaluateDeltaDynamic(delta)) | ||
return null | ||
} | ||
let delta | ||
for (const [key, sch] of Object.entries(node.anyOf)) { | ||
for (const [key, sch] of Object.entries(anyOf)) { | ||
const { sub, delta: deltaVariant } = subrule(suberr, current, sch, subPath('anyOf', key)) | ||
@@ -894,11 +829,9 @@ fun.write('if (%s) {', safenot(sub)) | ||
} | ||
if (node.anyOf.length > 0) evaluateDelta(delta) | ||
error({ path: ['anyOf'] }) | ||
mergeerror(suberr) | ||
node.anyOf.forEach(() => fun.write('}')) | ||
consume('anyOf', 'array') | ||
} | ||
if (anyOf.length > 0) evaluateDelta(delta) | ||
error({ path: ['anyOf'], suberr }) | ||
anyOf.forEach(() => fun.write('}')) | ||
return null | ||
}) | ||
if (node.oneOf !== undefined) { | ||
enforce(Array.isArray(node.oneOf), 'Invalid oneOf') | ||
handle('oneOf', ['array'], (oneOf) => { | ||
const passes = gensym('passes') | ||
@@ -909,13 +842,15 @@ fun.write('let %s = 0', passes) | ||
let i = 0 | ||
for (const [key, sch] of Object.entries(node.oneOf)) { | ||
const { sub, delta: deltaVariant } = subrule(suberr, current, sch, subPath('oneOf', key)) | ||
fun.write('if (%s) %s++', sub, passes) | ||
if (!includeErrors && i++ > 0) errorIf(format('%s > 1', passes), { path: ['oneOf'] }) | ||
delta = delta ? orDelta(delta, deltaVariant) : deltaVariant | ||
} | ||
if (node.oneOf.length > 0) evaluateDelta(delta) | ||
const entries = Object.entries(oneOf).map(([key, sch]) => { | ||
if (!includeErrors && i++ > 1) errorIf(format('%s > 1', passes), { path: ['oneOf'] }) | ||
const entry = subrule(suberr, current, sch, subPath('oneOf', key), dyn) | ||
fun.write('if (%s) %s++', entry.sub, passes) | ||
delta = delta ? orDelta(delta, entry.delta) : entry.delta | ||
return entry | ||
}) | ||
if (oneOf.length > 0) evaluateDelta(delta) | ||
errorIf(format('%s !== 1', passes), { path: ['oneOf'] }) | ||
fun.if(format('%s === 0', passes), () => mergeerror(suberr)) // if none matched, dump all errors | ||
consume('oneOf', 'array') | ||
} | ||
for (const entry of entries) fun.if(entry.sub, () => evaluateDeltaDynamic(entry.delta)) | ||
return null | ||
}) | ||
} | ||
@@ -925,4 +860,3 @@ | ||
const [funSize, unusedSize] = [fun.size(), unused.size] | ||
if (definitelyType(...validTypes)) checkBlock() | ||
else fun.if(queryType, checkBlock) | ||
fun.if(definitelyType(...validTypes) ? true : queryType, checkBlock) | ||
// enforce check that non-applicable blocks are empty and no rules were applied | ||
@@ -939,9 +873,12 @@ if (funSize !== fun.size() || unusedSize !== unused.size) | ||
} else if (node.unevaluatedItems || node.unevaluatedItems === false) { | ||
if (isDynamic(stat).items) throw new Error('Dynamic unevaluated is not implemented') | ||
const limit = format('%d', stat.items) | ||
additionalItems(limit, node.unevaluatedItems, 'unevaluatedItems') | ||
if (isDynamic(stat).items) { | ||
if (!opts[optDynamic]) throw new Error('Dynamic unevaluated tracing is not enabled') | ||
additionalItems('unevaluatedItems', format('Math.max(%d, ...%s)', stat.items, dyn.items)) | ||
} else { | ||
additionalItems('unevaluatedItems', format('%d', stat.items)) | ||
} | ||
} | ||
} | ||
const checkObjectsFinal = () => { | ||
prevWrap(node.patternProperties, () => { | ||
prevWrap(stat.patterns.length > 0 || stat.dyn.patterns.length > 0 || stat.unknown, () => { | ||
if (stat.properties.includes(true)) { | ||
@@ -951,5 +888,12 @@ // Everything is statically evaluated, so this check is unreachable. Allow only 'false' rule here. | ||
} else if (node.unevaluatedProperties || node.unevaluatedProperties === false) { | ||
if (isDynamic(stat).properties) throw new Error('Dynamic unevaluated is not implemented') | ||
const sawStatic = (key) => additionalCondition(key, stat.properties, stat.patterns) | ||
additionalProperties(sawStatic, node.unevaluatedProperties, 'unevaluatedProperties') | ||
const notStatic = (key) => additionalCondition(key, stat.properties, stat.patterns) | ||
if (isDynamic(stat).properties) { | ||
if (!opts[optDynamic]) throw new Error('Dynamic unevaluated tracing is not enabled') | ||
scope.propertyIn = functions.propertyIn | ||
const notDynamic = (key) => format('!propertyIn(%s, %s)', key, dyn.props) | ||
const condition = (key) => safeand(notStatic(key), notDynamic(key)) | ||
additionalProperties('unevaluatedProperties', condition) | ||
} else { | ||
additionalProperties('unevaluatedProperties', notStatic) | ||
} | ||
} | ||
@@ -962,7 +906,8 @@ }) | ||
const performValidation = () => { | ||
if (prev !== null) fun.write('let %s = errorCount', prev) | ||
if (prev !== null) fun.write('const %s = errorCount', prev) | ||
if (checkConst()) { | ||
// const/enum shouldn't have any other validation rules except for already checked type/$ref | ||
enforce(unused.size === 0, 'Unexpected keywords mixed with const or enum:', [...unused]) | ||
evaluateDelta({ properties: [true], items: Infinity }) // everything is evaluated for const | ||
const typeKeys = [...types.keys()] // we don't extract type from const/enum, it's enough that we know that it's present | ||
evaluateDelta({ properties: [true], items: Infinity, type: typeKeys, fullstring: true }) // everything is evaluated for const | ||
return | ||
@@ -977,29 +922,65 @@ } | ||
// evaluated: apply static + dynamic | ||
typeWrap(checkArraysFinal, ['array'], types.get('array')(name)) | ||
typeWrap(checkObjectsFinal, ['object'], types.get('object')(name)) | ||
// evaluated: propagate dynamic to parent dynamic (aka trace) | ||
// static to parent is merged via return value | ||
applyDynamicToDynamic(trace, local.items, local.props) | ||
} | ||
const typeExact = (type) => typeArray && typeArray.length === 1 && typeArray[0] === type | ||
if (current.type) | ||
enforce(typeExact(current.type), 'Only one type is allowed here:', current.type) | ||
const needTypeValidate = !current.type && typeArray !== null && !parentCheckedType(...typeArray) | ||
if (needTypeValidate) { | ||
const filteredTypes = typeArray.filter((t) => typeApplicable(t)) | ||
let typeIfAdded = false | ||
handle('type', ['string', 'array'], (type) => { | ||
const typearr = Array.isArray(type) ? type : [type] | ||
for (const t of typearr) enforce(typeof t === 'string' && types.has(t), 'Unknown type:', t) | ||
if (current.type) { | ||
enforce(functions.deepEqual(typearr, [current.type]), 'One type is allowed:', current.type) | ||
evaluateDelta({ type: [current.type] }) | ||
return null | ||
} | ||
if (parentCheckedType(...typearr)) return null | ||
const filteredTypes = typearr.filter((t) => typeApplicable(t)) | ||
if (filteredTypes.length === 0) fail('No valid types possible') | ||
const typeInvalid = safenot(safeor(...filteredTypes.map((t) => types.get(t)(name)))) | ||
errorIf(typeInvalid, { path: ['type'] }) | ||
} | ||
evaluateDelta({ type: typeArray }) | ||
if (node.type !== undefined) consume('type', 'string', 'array') | ||
evaluateDelta({ type: typearr }) // can be safely done here, filteredTypes already prepared | ||
typeIfAdded = true | ||
return safenot(safeor(...filteredTypes.map((t) => types.get(t)(name)))) | ||
}) | ||
// If type validation was needed and did not return early, wrap this inside an else clause. | ||
if (needTypeValidate && allErrors) fun.block('else {', [], '}', performValidation) | ||
if (typeIfAdded && allErrors) fun.block('else {', [], '}', performValidation) | ||
else performValidation() | ||
finish() | ||
return stat // return statically evaluated | ||
if (!isSub) { | ||
if (!stat.type) enforceValidation('type') | ||
if (typeApplicable('array') && stat.items !== Infinity) | ||
enforceValidation(node.items ? 'additionalItems or unevaluatedItems' : 'items rule') | ||
if (typeApplicable('object') && !stat.properties.includes(true)) | ||
enforceValidation('additionalProperties or unevaluatedProperties') | ||
if (typeof node.propertyNames !== 'object') | ||
for (const sub of ['additionalProperties', 'unevaluatedProperties']) | ||
if (node[sub]) enforceValidation(`wild-card ${sub}`, 'requires propertyNames') | ||
if (!stat.fullstring && requireStringValidation) { | ||
const stringWarning = 'pattern, format or contentSchema must be specified for strings' | ||
fail(`[requireStringValidation] ${stringWarning}, use pattern: ^[\\s\\S]*$ to opt-out`) | ||
} | ||
} else { | ||
const n0 = schemaPath[schemaPath.length - 1] | ||
const n1 = schemaPath[schemaPath.length - 2] | ||
const allowed0 = ['not', 'if', 'then', 'else'] | ||
const allowed1 = ['oneOf', 'anyOf', 'allOf', 'dependencies', 'dependentSchemas'] | ||
// Sanity check, unreachable, double-check that we came from expected path | ||
enforce(allowed0.includes(n0) || allowed1.includes(n1), 'Unexpected') | ||
} | ||
return finish(local) | ||
} | ||
const stat = visit(format('validate.errors'), [], { name: safe('data') }, schema, []) | ||
const { stat, local } = visit(format('validate.errors'), [], { name: safe('data') }, schema, []) | ||
// evaluated: return dynamic for refs | ||
if (opts[optDynamic] && (isDynamic(stat).items || isDynamic(stat).properties)) { | ||
if (!local) throw new Error('Failed to trace dynamic properties') // Unreachable | ||
fun.write('validate.evaluatedDynamic = [%s, %s]', local.items, local.props) | ||
} | ||
if (allErrors) { | ||
@@ -1019,2 +1000,17 @@ fun.write('return errorCount === 0') | ||
const compile = (schema, opts) => { | ||
try { | ||
const scope = Object.create(null) | ||
return { scope, ref: compileSchema(schema, schema, opts, scope) } | ||
} catch (e) { | ||
// For performance, we try to build the schema without dynamic tracing first, then re-run with | ||
// it enabled if needed. Enabling it without need can give up to about 40% performance drop. | ||
if (e.message === 'Dynamic unevaluated tracing is not enabled') { | ||
const scope = Object.create(null) | ||
return { scope, ref: compileSchema(schema, schema, { ...opts, [optDynamic]: true }, scope) } | ||
} | ||
throw e | ||
} | ||
} | ||
module.exports = { compile } |
@@ -11,4 +11,3 @@ 'use strict' | ||
const options = { ...opts, schemas: buildSchemas(schemas || []), isJSON: isJSON || jsonCheck } | ||
const scope = Object.create(null) | ||
const ref = compile(schema, schema, options, scope) | ||
const { scope, ref } = compile(schema, options) | ||
if (opts.dryRun) return | ||
@@ -15,0 +14,0 @@ const fun = genfun() |
@@ -19,3 +19,3 @@ 'use strict' | ||
// https://json-schema.org/understanding-json-schema/reference/generic.html | ||
...['description', 'title', 'examples', '$comment'], // unused | ||
...['deprecated', 'description', 'title', 'examples', '$comment'], // unused | ||
] | ||
@@ -22,0 +22,0 @@ |
@@ -78,4 +78,8 @@ 'use strict' | ||
const safeor = safewrap((...args) => args.join(' || ') || 'false') | ||
const safeand = safewrap((...args) => args.join(' && ') || 'true') | ||
const safeor = safewrap( | ||
(...args) => (args.some((arg) => `${arg}` === 'true') ? 'true' : args.join(' || ') || 'false') | ||
) | ||
const safeand = safewrap( | ||
(...args) => (args.some((arg) => `${arg}` === 'false') ? 'false' : args.join(' && ') || 'true') | ||
) | ||
const safenot = (arg) => { | ||
@@ -82,0 +86,0 @@ if (`${arg}` === 'true') return safe('false') |
@@ -62,20 +62,6 @@ 'use strict' | ||
// Fast in Node.js, awful in browsers, no reason to optimize now. Work-around: polyfill Buffer | ||
const deBase64 = (string) => { | ||
if (typeof Buffer !== 'undefined') return Buffer.from(string, 'base64').toString('utf-8') | ||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' | ||
const map = Array(128) | ||
chars.split('').forEach((c, i) => (map[c.charCodeAt(0)] = i.toString(4).padStart(3, 0))) | ||
let tmp = '' | ||
const bytes = new Uint8Array(Math.floor((string.length * 3) / 4)) | ||
let filled = 0 | ||
for (let i = 0; i < string.length; i++) { | ||
tmp += map[string.charCodeAt(i)] || '' | ||
if (tmp.length >= 4) { | ||
bytes[filled++] = parseInt(tmp.slice(0, 4), 4) | ||
tmp = tmp.slice(4) | ||
} | ||
} | ||
const view = new Uint8Array(bytes.buffer, bytes.byteOffset, Math.min(filled, bytes.length)) | ||
return new TextDecoder('utf-8').decode(view) | ||
const b = atob(string) | ||
return new TextDecoder('utf-8').decode(new Uint8Array(b.length).map((_, i) => b.charCodeAt(i))) | ||
} | ||
@@ -91,9 +77,13 @@ | ||
const errorMerge = ({ keywordLocation, instanceLocation, ...more }, schemaBase, dataBase) => ({ | ||
const errorMerge = ({ keywordLocation, instanceLocation }, schemaBase, dataBase) => ({ | ||
keywordLocation: `${schemaBase}${keywordLocation.slice(1)}`, | ||
instanceLocation: `${dataBase}${instanceLocation.slice(1)}`, | ||
...more, | ||
}) | ||
const errorUtils = { toPointer, pointerPart, errorMerge } | ||
module.exports = { stringLength, isMultipleOf, deepEqual, unique, deBase64, hasOwn, ...errorUtils } | ||
const propertyIn = (key, [properties, patterns]) => | ||
properties.includes(true) || | ||
properties.some((prop) => prop === key) || | ||
patterns.some((pattern) => new RegExp(pattern, 'u').test(key)) | ||
const extraUtils = { toPointer, pointerPart, errorMerge, propertyIn } | ||
module.exports = { stringLength, isMultipleOf, deepEqual, unique, deBase64, hasOwn, ...extraUtils } |
@@ -15,3 +15,3 @@ 'use strict' | ||
const initTracing = () => ({ | ||
...{ properties: [], patterns: [], required: [], items: 0, type: null }, | ||
...{ properties: [], patterns: [], required: [], items: 0, type: null, fullstring: false }, | ||
dyn: { properties: [], patterns: [], items: 0 }, | ||
@@ -23,2 +23,3 @@ unknown: false, | ||
const wrapFun = (f) => (...args) => f(...args.map(wrap)) | ||
const stringValidated = (A) => A.fullstring || (A.type && !A.type.includes('string')) | ||
@@ -32,2 +33,3 @@ // Result means that both sets A and B are correct | ||
type: A.type && B.type ? [...new Set([...A.type, ...B.type])] : null, | ||
fullstring: stringValidated(A) || stringValidated(B), | ||
dyn: { | ||
@@ -63,2 +65,3 @@ items: Math.max(A.dyn.items, B.dyn.items), | ||
type: A.type && B.type ? A.type.filter((x) => B.type.includes(x)) : null, | ||
fullstring: stringValidated(A) && stringValidated(B), | ||
dyn: { | ||
@@ -79,2 +82,3 @@ items: Math.max(A.items, B.items, A.dyn.items, B.dyn.items), | ||
stat.type = stat.type ? stat.type.filter((x) => delta.type.includes(x)) : delta.type | ||
if (delta.fullstring || (stat.type && !stat.type.includes('string'))) stat.fullstring = true | ||
if (delta.dyn) stat.dyn.items = Math.max(stat.dyn.items, delta.dyn.items) | ||
@@ -87,6 +91,6 @@ if (delta.dyn) stat.dyn.properties.push(...delta.dyn.properties) | ||
const isDynamic = wrapFun(({ unknown, items, dyn, ...stat }) => ({ | ||
items: unknown || dyn.items > items, | ||
properties: unknown || !inProperties(stat, dyn), | ||
items: items !== Infinity && (unknown || dyn.items > items), | ||
properties: !stat.properties.includes(true) && (unknown || !inProperties(stat, dyn)), | ||
})) | ||
module.exports = { initTracing, andDelta, orDelta, applyDelta, isDynamic } | ||
module.exports = { initTracing, andDelta, orDelta, applyDelta, isDynamic, inProperties } |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
91758
1702