Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@exodus/schemasafe

Package Overview
Dependencies
Maintainers
36
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@exodus/schemasafe - npm Package Compare versions

Comparing version 1.0.0-alpha.4 to 1.0.0-beta.1

src/compile.js

12

package.json
{
"name": "@exodus/schemasafe",
"version": "1.0.0-alpha.4",
"version": "1.0.0-beta.1",
"description": "JSON Safe Parser & Schema Validator",

@@ -16,10 +16,11 @@ "license": "MIT",

"files": [
"src/compile.js",
"src/formats.js",
"src/generate-function.js",
"src/safe-format.js",
"src/index.js",
"src/jaystring.js",
"src/formats.js",
"src/scope-functions.js",
"src/known-keywords.js",
"src/pointer.js",
"src/index.js"
"src/safe-format.js",
"src/scope-functions.js"
],

@@ -30,2 +31,3 @@ "scripts": {

"coverage": "c8 --reporter=lcov --reporter=text npm run test",
"coverage:lcov": "c8 --reporter=lcovonly npm run test",
"test": "npm run test:normal | tap-spec && npm run test:module | tap-spec",

@@ -32,0 +34,0 @@ "test:raw": "npm run test:normal && npm run test:module",

# `@exodus/schemasafe`
A [JSONSchema](https://json-schema.org/) validator that uses code generation to be extremely fast.
A code-generating [JSON Schema](https://json-schema.org/) validator that attempts to be reasonably secure.
Supports [draft-04/06/07](doc/Specification-support.md).
[![Node CI Status](https://github.com/ExodusMovement/schemasafe/workflows/Node%20CI/badge.svg)](https://github.com/ExodusMovement/schemasafe/actions)
[![npm](https://img.shields.io/npm/v/@exodus/schemasafe.svg)](https://www.npmjs.com/package/@exodus/schemasafe)
[![codecov](https://codecov.io/gh/ExodusMovement/schemasafe/branch/master/graph/badge.svg)](https://codecov.io/gh/ExodusMovement/schemasafe)

@@ -32,6 +36,2 @@ ## Installation

console.log('should not be valid', validate({}))
// get the last list of errors by checking validate.errors
// the following will print [{field: 'data.hello', message: 'is required'}]
console.log(validate.errors)
```

@@ -78,8 +78,10 @@

## Verbose mode shows more information about the source of the error
## Enabling errors shows information about the source of the error
When the `verbose` options is set to `true`, `@exodus/schemasafe` also outputs:
When the `includeErrors` option is set to `true`, `@exodus/schemasafe` also outputs:
- `value`: The data value that caused the error
- `schemaPath`: a JSON pointer string as an URI fragment indicating which sub-schema failed, e.g. `#/type`
- `schemaPath`: a JSON pointer string as an URI fragment indicating which sub-schema failed, e.g.
`#/properties/item/type`
- `dataPath`: a JSON pointer string as an URI fragment indicating which property of the object
failed validation, e.g. `#/item`

@@ -96,17 +98,12 @@ ```js

}
const validate = validator(schema, {
includeErrors: true,
verboseErrors: true
})
const validate = validator(schema, { includeErrors: true })
validate({ hello: 100 });
console.log(validate.errors)
// [ { field: 'data["hello"]',
// message: 'is the wrong type',
// type: 'string',
// schemaPath: '#/properties/hello',
// value: 100 } ]
// [ { schemaPath: '#/properties/hello/type',
// dataPath: '#/hello' } ]
```
See [Error handling](./doc/Error-handling.md) for more information.
## Generate Modules

@@ -138,6 +135,5 @@

* if (data === undefined) data = null
* let errors = 0
* if (!(typeof data === "string")) return false
* if (!format0(data)) return false
* return errors === 0
* return true
* })})();

@@ -149,4 +145,8 @@ */

`@exodus/schemasafe` uses code generation to turn a JSON schema into javascript code that is easily optimizeable by v8.
`@exodus/schemasafe` uses code generation to turn a JSON schema into javascript code that is easily
optimizeable by v8.
See [Performance](./doc/Performance.md) for information on options that might affect performace
both ways.
## Previous work

@@ -153,0 +153,0 @@

@@ -11,7 +11,8 @@ 'use strict'

if (name[0] === '.' || name.endsWith('.') || name.includes('..')) return false
if (!/^[a-z0-9.-]+$/i.test(host) || !/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+/.test(name)) return false
if (!/^[a-z0-9.-]+$/i.test(host) || !/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+$/i.test(name)) return false
return host.split('.').every((part) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(part))
},
// matches ajv + length checks
hostname: (host) => {
hostname: (input) => {
const host = input.endsWith('.') ? input.slice(0, input.length - 1) : input
if (host.length > 253) return false

@@ -64,9 +65,18 @@ if (!/^[a-z0-9.-]+$/i.test(host)) return false

// matches ajv + length checks
// matches ajv + unwrap nested group
// uuid: http://tools.ietf.org/html/rfc4122
uuid: (input) =>
input.length <= 36 + 9 &&
/^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(input),
uuid: /^(?:urn:uuid:)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
// TODO: iri, iri-reference, idn-email, idn-hostname, duration
// length restriction is an arbitrary safeguard
// first regex verifies structure
// second regex verifies no more than one fraction, and that at least 1 block is present
duration: (input) =>
input.length > 1 &&
input.length < 80 &&
/^P(?:[.,\d]+Y)?(?:[.,\d]+M)?(?:[.,\d]+W)?(?:[.,\d]+D)?(?:T(?:[.,\d]+H)?(?:[.,\d]+M)?(?:[.,\d]+S)?)?$/.test(
input
) &&
/^P[\dYMWDTHMS]*(?:\d[.,]\d+)?[YMWDTHMS]$/.test(input),
// TODO: iri, iri-reference, idn-email, idn-hostname
}

@@ -87,12 +97,2 @@

// other
'utc-millisec': /^[0-9]{1,15}\.?[0-9]{0,15}$/,
phone: (input) => {
if (input.length > 30) return false
if (!/^\+[0-9][0-9 ]{5,27}[0-9]$/.test(input)) return false
if (/ {2}/.test(input)) return false
const digits = input.substring(1).replace(/ /g, '').length
return digits >= 7 && digits <= 15
},
// ajv has /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i, this is equivalent

@@ -107,3 +107,3 @@ // uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A

// manually cleaned up from is-my-json-valid, CSS 2.1 colors only per draft03 spec
color: /^(?:#[0-9A-Fa-f]{3,6}|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|rgb\(\s*(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*,\s*(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*,\s*(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*\)|rgb\(\s*(?:\d?\d%|100%)+\s*,\s*(?:\d?\d%|100%)+\s*,\s*(?:\d?\d%|100%)+\s*\))$/,
color: /^(?:#[0-9A-Fa-f]{3,6}|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|rgb\(\s*(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*,\s*(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*,\s*(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*\)|rgb\(\s*(?:\d?\d%|100%)\s*,\s*(?:\d?\d%|100%)\s*,\s*(?:\d?\d%|100%)\s*\))$/,

@@ -110,0 +110,0 @@ // style is deliberately unsupported, don't accept untrusted styles

'use strict'
const { format } = require('./safe-format')
const { format, safe } = require('./safe-format')
const jaystring = require('./jaystring')

@@ -58,3 +58,5 @@

makeModule(scope = {}) {
const scopeDefs = processScope(scope).map(([key, val]) => `const ${key} = ${jaystring(val)};`)
const scopeDefs = processScope(scope).map(
([key, val]) => `const ${safe(key)} = ${jaystring(val)};`
)
return `(function() {\n'use strict'\n${scopeDefs.join('\n')}\n${build()}})();`

@@ -61,0 +63,0 @@ },

'use strict'
const { format, safe, safeand, safeor } = require('./safe-format')
const genfun = require('./generate-function')
const { toPointer, resolveReference, joinPath } = require('./pointer')
const formats = require('./formats')
const { buildSchemas } = require('./pointer')
const { compile } = require('./compile')
const functions = require('./scope-functions')
const KNOWN_KEYWORDS = require('./known-keywords')
// for building into the validation function
const types = new Map(
Object.entries({
null: (name) => format('%s === null', name),
boolean: (name) => format('typeof %s === "boolean"', name),
array: (name) => format('Array.isArray(%s)', name),
object: (n) => format('typeof %s === "object" && %s && !Array.isArray(%s)', n, n, n),
number: (name) => format('typeof %s === "number"', name),
integer: (name) => format('Number.isInteger(%s)', name),
string: (name) => format('typeof %s === "string"', name),
})
)
const validator = (schema, { jsonCheck = false, isJSON = false, schemas, ...opts } = {}) => {
if (jsonCheck && isJSON) throw new Error('Can not specify both isJSON and jsonCheck options')
const options = { ...opts, schemas: buildSchemas(schemas || []), isJSON: isJSON || jsonCheck }
const scope = Object.create(null)
const actualValidate = compile(schema, schema, options, scope)
if (!jsonCheck || opts.dryRun) return actualValidate
// for checking schema parts in consume()
const schemaTypes = new Map(
Object.entries({
boolean: (arg) => typeof arg === 'boolean',
array: (arg) => Array.isArray(arg),
object: (arg) => typeof arg === 'object' && arg && !Array.isArray(arg),
finite: (arg) => Number.isFinite(arg),
integer: (arg) => Number.isInteger(arg),
natural: (arg) => Number.isInteger(arg) && arg >= 0,
string: (arg) => typeof arg === 'string',
jsonval: (arg) => functions.deepEqual(arg, JSON.parse(JSON.stringify(arg))),
})
)
const scopeCache = Symbol('cache')
// Order is important, newer at the top!
const schemaVersions = [
'https://json-schema.org/draft/2019-09/schema',
'https://json-schema.org/draft-07/schema',
'https://json-schema.org/draft-06/schema',
'https://json-schema.org/draft-04/schema',
'https://json-schema.org/draft-03/schema',
]
const noopRegExps = new Set(['^[\\s\\S]*$', '^[\\S\\s]*$', '^[^]*$', '', '.*'])
// Helper methods for semi-structured paths
const propvar = (parent, keyname, inKeys = false) => Object.freeze({ parent, keyname, inKeys }) // property by variable
const propimm = (parent, keyval) => Object.freeze({ parent, keyval }) // property by immediate value
const buildName = ({ name, parent, keyval, keyname }) => {
if (name) {
if (parent || keyval || keyname) throw new Error('name can be used only stand-alone')
return name // top-level
}
if (keyval && keyname) throw new Error('Can not use key value and name at the same time')
if (!parent) throw new Error('Can not use property of undefined parent!')
if (parent && keyval !== undefined) {
if (!['string', 'number'].includes(typeof keyval)) throw new Error('Invalid property path')
return format('%s[%j]', buildName(parent), keyval)
} else if (parent && keyname) {
return format('%s[%s]', buildName(parent), keyname)
}
/* c8 ignore next */
throw new Error('Unreachable')
}
const rootMeta = new WeakMap()
const compile = (schema, root, opts, scope, basePathRoot) => {
const {
mode = 'default',
useDefaults = false,
removeAdditional = false, // supports additionalProperties: false and additionalItems: false
includeErrors: optIncludeErrors = false,
allErrors: optAllErrors = false,
verboseErrors = false,
dryRun = false,
allowUnusedKeywords = opts.mode === 'lax',
requireValidation = opts.mode === 'strong',
requireStringValidation = opts.mode === 'strong',
complexityChecks = opts.mode === 'strong',
isJSON: optIsJSON = false, // assume input to be JSON, which e.g. makes undefined impossible
jsonCheck = false, // disabled by default, it's assumed that data is from JSON.parse
$schemaDefault = null,
formats: optFormats = {},
weakFormats = opts.mode !== 'strong',
extraFormats = false,
schemas = {},
...unknown
} = opts
const fmts = {
...formats.core,
...(weakFormats ? formats.weak : {}),
...(extraFormats ? formats.extra : {}),
...optFormats,
}
if (Object.keys(unknown).length !== 0)
throw new Error(`Unknown options: ${Object.keys(unknown).join(', ')}`)
if (!['strong', 'lax', 'default'].includes(mode)) throw new Error(`Invalid mode: ${mode}`)
if (mode === 'strong' && (!requireValidation || !requireStringValidation || !complexityChecks))
throw new Error('Strong mode demands require(String)Validation and complexityChecks')
if (mode === 'strong' && (weakFormats || allowUnusedKeywords))
throw new Error('Strong mode forbids weakFormats and allowUnusedKeywords')
if (optIsJSON && jsonCheck)
throw new Error('Can not specify both isJSON and jsonCheck options, please choose one')
const isJSON = optIsJSON || jsonCheck
if (!scope[scopeCache])
scope[scopeCache] = { sym: new Map(), ref: new Map(), format: new Map(), pattern: new Map() }
const cache = scope[scopeCache] // cache meta info for known scope variables, per meta type
const gensym = (name) => {
if (!cache.sym.get(name)) cache.sym.set(name, 0)
const index = cache.sym.get(name)
cache.sym.set(name, index + 1)
return safe(`${name}${index}`)
}
const patterns = (p) => {
if (cache.pattern.has(p)) return cache.pattern.get(p)
const n = gensym('pattern')
scope[n] = new RegExp(p, 'u')
cache.pattern.set(p, n)
return n
}
const vars = 'ijklmnopqrstuvxyz'.split('')
const genloop = () => {
const v = vars.shift()
vars.push(v + v[0])
return safe(v)
}
const present = (location) => {
const name = buildName(location) // also checks for sanity, do not remove
const { parent, keyval, keyname, inKeys } = location
if (inKeys) {
/* c8 ignore next */
if (isJSON) throw new Error('Unreachable: useless check, can not be undefined')
return format('%s !== undefined', name)
}
if (parent && keyname) {
scope.hasOwn = functions.hasOwn
return format('%s !== undefined && hasOwn(%s, %s)', name, buildName(parent), keyname)
} else if (parent && keyval !== undefined) {
scope.hasOwn = functions.hasOwn
return format('%s !== undefined && hasOwn(%s, %j)', name, buildName(parent), keyval)
}
/* c8 ignore next */
throw new Error('Unreachable: present() check without parent')
}
// jsonCheck wrapper implementation below
scope.deepEqual = functions.deepEqual
scope.actualValidate = actualValidate
const fun = genfun()
fun.write('function validate(data) {')
// Since undefined is not a valid JSON value, we coerce to null and other checks will catch this
fun.write('if (data === undefined) data = null')
if (optIncludeErrors) fun.write('validate.errors = null')
fun.write('let errors = 0')
let jsonCheckPerformed = false
const getMeta = () => rootMeta.get(root) || {}
const basePathStack = basePathRoot ? [basePathRoot] : []
const visit = (allErrors, includeErrors, history, current, node, schemaPath) => {
// e.g. top-level data and property names, OR already checked by present() in history, OR in keys and not undefined
const definitelyPresent =
!current.parent || history.includes(current) || (current.inKeys && isJSON)
const name = buildName(current)
const currPropVar = (...args) => propvar(current, ...args)
const currPropImm = (...args) => propimm(current, ...args)
const error = (msg, prop, value) => {
if (includeErrors === true) {
const errorObj = { field: prop || name, message: msg, schemaPath: toPointer(schemaPath) }
const errorJS = verboseErrors
? format('{ ...%j, value: %s }', errorObj, value || name)
: format('%j', errorObj)
if (allErrors) {
fun.write('if (validate.errors === null) validate.errors = []')
fun.write('validate.errors.push(%s)', errorJS)
} else {
// Array assignment is significantly faster, do not refactor the two branches
fun.write('validate.errors = [%s]', errorJS)
}
}
if (allErrors) {
fun.write('errors++')
} else {
fun.write('return false')
}
}
const errorIf = (fmt, args, ...errorArgs) => {
const condition = format(fmt, ...args)
if (includeErrors === false) {
// in this case, we can fast-track and inline this to generate more readable code
if (allErrors) {
fun.write('if (%s) errors++', condition)
} else {
fun.write('if (%s) return false', condition)
}
} else {
fun.write('if (%s) {', condition)
error(...errorArgs)
fun.write('}')
}
}
const fail = (msg, value) => {
const comment = value !== undefined ? ` ${JSON.stringify(value)}` : ''
throw new Error(`${msg}${comment} at ${toPointer(schemaPath)}`)
}
const enforce = (ok, ...args) => ok || fail(...args)
const enforceValidation = (msg) => enforce(!requireValidation, `[requireValidation] ${msg}`)
const subPath = (...args) => [...schemaPath, ...args]
// JSON check is once only for the top-level object, before everything else
if (jsonCheck && !jsonCheckPerformed) {
/* c8 ignore next */
if (`${name}` !== 'data') throw new Error('Unreachable: invalid json check')
scope.deepEqual = functions.deepEqual
errorIf('!deepEqual(%s, JSON.parse(JSON.stringify(%s)))', [name, name], 'not JSON compatible')
jsonCheckPerformed = true
}
if (typeof node === 'boolean') {
if (node === true) {
// any is valid
enforceValidation('schema = true is not allowed')
} else if (definitelyPresent) {
// node === false always fails in this case
error('is unexpected')
} else {
// node === false
errorIf('%s', [present(current)], 'is unexpected')
}
return
}
enforce(Object.getPrototypeOf(node) === Object.prototype, 'Schema is not an object')
for (const key of Object.keys(node))
enforce(KNOWN_KEYWORDS.includes(key) || allowUnusedKeywords, 'Keyword not supported:', key)
if (Object.keys(node).length === 0) {
enforceValidation('empty rules node encountered')
return // nothing to validate here, basically the same as node === true
}
const unused = new Set(Object.keys(node))
const consume = (prop, ...ruleTypes) => {
enforce(unused.has(prop), 'Unexpected double consumption:', prop)
enforce(functions.hasOwn(node, prop), 'Is not an own property:', prop)
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)
unused.delete(prop)
}
const finish = () => {
if (!definitelyPresent) fun.write('}') // undefined check
enforce(unused.size === 0 || allowUnusedKeywords, 'Unprocessed keywords:', [...unused])
}
if (node === root) {
const $schema = node.$schema || $schemaDefault
if (node.$schema) {
if (typeof node.$schema !== 'string') throw new Error('Unexpected $schema')
consume('$schema', 'string')
}
if ($schema) {
const version = $schema.replace(/^http:\/\//, 'https://').replace(/#$/, '')
enforce(schemaVersions.includes(version), 'Unexpected schema version:', version)
const schemaIsOlderThan = (ver) =>
schemaVersions.indexOf(version) >
schemaVersions.indexOf(`https://json-schema.org/${ver}/schema`)
rootMeta.set(root, {
exclusiveRefs: schemaIsOlderThan('draft/2019-09'),
booleanRequired: schemaIsOlderThan('draft-04'),
})
}
}
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
// 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')
}
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 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) {
fun.write('if (!(%s)) {', present(current))
if (defaultIsPresent) {
fun.write('%s = %j', name, node.default)
consume('default', 'jsonval')
}
if (booleanRequired) {
if (node.required === true) {
if (!defaultIsPresent) error('is required')
consume('required', 'boolean')
} else if (node.required === false) {
consume('required', 'boolean')
}
}
fun.write('} else {')
} else {
fun.write('if (%s) {', present(current))
}
if (node.$ref) {
const resolved = resolveReference(root, schemas || {}, node.$ref, basePath())
const [sub, subRoot, path] = resolved[0] || []
if (sub || sub === false) {
let n = cache.ref.get(sub)
if (!n) {
n = gensym('ref')
cache.ref.set(sub, n)
let fn = null // resolve cyclic dependencies
scope[n] = (...args) => fn(...args)
const override = { includeErrors: false, jsonCheck: false, isJSON }
fn = compile(sub, subRoot, { ...opts, ...override }, scope, path)
scope[n] = fn
}
errorIf('!%s(%s)', [n, name], 'referenced schema does not match')
} else {
fail('failed to resolve $ref:', node.$ref)
}
consume('$ref', 'string')
if (getMeta().exclusiveRefs) {
// ref overrides any sibling keywords for older schemas
finish()
return
}
}
/* 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 means no type validation, which is required if we don't have const or enum
if (typeArray === null && node.const === undefined && !node.enum && !hasSubValidation)
enforceValidation('type is required')
const typeApplicable = (...possibleTypes) =>
typeArray === null || typeArray.some((x) => possibleTypes.includes(x))
const makeCompare = (variableName, complex) => {
if (complex) {
scope.deepEqual = functions.deepEqual
return (e) => format('deepEqual(%s, %j)', variableName, e)
}
return (e) => format('(%s === %j)', variableName, e)
}
const enforceRegex = (pattern, target = node) => {
enforce(typeof pattern === 'string', 'Invalid pattern:', pattern)
if (requireValidation || requireStringValidation)
enforce(/^\^.*\$$/.test(pattern), 'Should start with ^ and end with $:', pattern)
if (complexityChecks && (pattern.match(/[{+*]/g) || []).length > 1)
enforce(target.maxLength !== undefined, 'maxLength should be specified for:', pattern)
}
// Can not be used before undefined check above! The one performed by present()
const rule = (...args) => visit(allErrors, includeErrors, [...history, current], ...args)
const subrule = (...args) => visit(true, false, [...history, current], ...args)
/* Checks inside blocks are independent, they are happening on the same code depth */
const checkNumbers = () => {
const applyMinMax = (value, operator, message) => {
enforce(Number.isFinite(value), 'Invalid minimum or maximum:', value)
errorIf('!(%d %c %s)', [value, operator, name], message)
}
if (Number.isFinite(node.exclusiveMinimum)) {
applyMinMax(node.exclusiveMinimum, '<', 'is less than exclusiveMinimum')
consume('exclusiveMinimum', 'finite')
} else if (node.minimum !== undefined) {
applyMinMax(node.minimum, node.exclusiveMinimum ? '<' : '<=', 'is less than minimum')
consume('minimum', 'finite')
if (typeof node.exclusiveMinimum === 'boolean') consume('exclusiveMinimum', 'boolean')
}
if (Number.isFinite(node.exclusiveMaximum)) {
applyMinMax(node.exclusiveMaximum, '>', 'is more than exclusiveMaximum')
consume('exclusiveMaximum', 'finite')
} else if (node.maximum !== undefined) {
applyMinMax(node.maximum, node.exclusiveMaximum ? '>' : '>=', 'is more than 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) {
enforce(Number.isFinite(node[multipleOf]), `Invalid ${multipleOf}:`, node[multipleOf])
scope.isMultipleOf = functions.isMultipleOf
errorIf('!isMultipleOf(%s, %d)', [name, node[multipleOf]], 'has a remainder')
consume(multipleOf, 'finite')
}
}
const checkStrings = () => {
if (node.maxLength !== undefined) {
enforce(Number.isFinite(node.maxLength), 'Invalid maxLength:', node.maxLength)
scope.stringLength = functions.stringLength
errorIf('stringLength(%s) > %d', [name, node.maxLength], 'has longer length than allowed')
consume('maxLength', 'natural')
}
if (node.minLength !== undefined) {
enforce(Number.isFinite(node.minLength), 'Invalid minLength:', node.minLength)
scope.stringLength = functions.stringLength
errorIf('stringLength(%s) < %d', [name, node.minLength], 'has less length than allowed')
consume('minLength', 'natural')
}
if (node.format && functions.hasOwn(fmts, node.format)) {
const formatImpl = fmts[node.format]
if (formatImpl instanceof RegExp || typeof formatImpl === 'function') {
let n = cache.format.get(formatImpl)
if (!n) {
n = gensym('format')
scope[n] = formatImpl
cache.format.set(formatImpl, n)
}
if (formatImpl instanceof RegExp) {
// built-in formats are fine, check only ones from options
if (functions.hasOwn(optFormats, node.format)) enforceRegex(formatImpl.source)
errorIf('!%s.test(%s)', [n, name], `must be ${node.format} format`)
} else {
errorIf('!%s(%s)', [n, name], `must be ${node.format} format`)
}
} else {
fail('Unrecognized format used:', node.format)
}
consume('format', 'string')
} else {
enforce(!node.format, 'Unrecognized format used:', node.format)
}
if (node.pattern) {
enforceRegex(node.pattern)
if (!noopRegExps.has(node.pattern)) {
const p = patterns(node.pattern)
errorIf('!%s.test(%s)', [p, name], 'pattern mismatch')
}
consume('pattern', 'string')
}
const stringValidated = node.format || node.pattern || hasSubValidation
if (typeApplicable('string') && requireStringValidation && !stringValidated) {
fail('pattern or format must be specified for strings, 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('%s.length > %d', [name, node.maxItems], 'has more items than allowed')
consume('maxItems', 'natural')
}
if (node.minItems !== undefined) {
enforce(Number.isFinite(node.minItems), 'Invalid minItems:', node.minItems)
// can be higher that .items length with additionalItems
errorIf('%s.length < %d', [name, node.minItems], 'has less items than allowed')
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}`))
} else {
const i = genloop()
fun.block('for (let %s = 0; %s < %s.length; %s++) {', [i, i, name, i], '}', () => {
rule(currPropVar(i), node.items, subPath('items'))
})
}
consume('items', 'object', 'array', 'boolean')
} else if (typeApplicable('array') && !hasSubValidation) {
enforceValidation('items rule must be specified')
}
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 === false) {
const limit = node.items.length
if (removeAdditional) {
fun.write('if (%s.length > %d) %s.length = %d', name, limit, name, limit)
} else {
errorIf('%s.length > %d', [name, limit], 'has additional items')
}
consume('additionalItems', 'boolean')
} else if (node.additionalItems) {
const i = genloop()
const offset = node.items.length
fun.block('for (let %s = %d; %s < %s.length; %s++) {', [i, offset, i, name, i], '}', () => {
rule(currPropVar(i), node.additionalItems, subPath('additionalItems'))
})
consume('additionalItems', 'object', 'boolean')
} else if (node.items.length === node.maxItems) {
// No additional items are possible
} else {
enforceValidation('additionalItems rule must be specified for fixed arrays')
}
if (node.contains || node.contains === false) {
const prev = gensym('prev')
const passes = gensym('passes')
fun.write('let %s = 0', passes)
const i = genloop()
fun.block('for (let %s = 0; %s < %s.length; %s++) {', [i, i, name, i], '}', () => {
fun.write('const %s = errors', prev)
subrule(currPropVar(i), node.contains, subPath('contains'))
fun.write('if (%s === errors) { %s++ } else errors = %s', prev, passes, prev)
})
if (Number.isFinite(node.minContains)) {
errorIf('%s < %d', [passes, node.minContains], 'array contains too few matching items')
consume('minContains', 'natural')
} else {
errorIf('%s < 1', [passes], 'array does not contain a match')
}
if (Number.isFinite(node.maxContains)) {
errorIf('%s > %d', [passes, node.maxContains], 'array contains too many matching items')
consume('maxContains', 'natural')
}
consume('contains', 'object', 'boolean')
}
const isSimpleForUnique = () => {
if (node.maxItems !== undefined) return true
if (typeof node.items === 'object') {
if (Array.isArray(node.items) && node.additionalItems === false) return true
if (!Array.isArray(node.items) && node.items.type) {
const itemTypes = Array.isArray(node.items.type) ? node.items.type : [node.items.type]
const primitiveTypes = ['null', 'boolean', 'number', 'integer', 'string']
if (itemTypes.every((itemType) => primitiveTypes.includes(itemType))) return true
}
}
return false
}
if (node.uniqueItems === true) {
if (complexityChecks)
enforce(isSimpleForUnique(), 'maxItems should be specified for non-primitive uniqueItems')
scope.unique = functions.unique
scope.deepEqual = functions.deepEqual
errorIf('!unique(%s)', [name], 'must be unique')
consume('uniqueItems', 'boolean')
} else if (node.uniqueItems === false) {
consume('uniqueItems', 'boolean')
}
}
const checkObjects = () => {
if (node.maxProperties !== undefined) {
enforce(Number.isFinite(node.maxProperties), 'Invalid maxProperties:', node.maxProperties)
errorIf('Object.keys(%s).length > %d', [name, node.maxProperties], 'too many properties')
consume('maxProperties', 'natural')
}
if (node.minProperties !== undefined) {
enforce(Number.isFinite(node.minProperties), 'Invalid minProperties:', node.minProperties)
errorIf('Object.keys(%s).length < %d', [name, node.minProperties], 'too few properties')
consume('minProperties', 'natural')
}
if (typeof node.propertyNames === 'object' || typeof node.propertyNames === 'boolean') {
const key = gensym('key')
fun.block('for (const %s of Object.keys(%s)) {', [key, name], '}', () => {
const names = node.propertyNames
const nameSchema = typeof names === 'object' ? { type: 'string', ...names } : names
rule({ name: key }, nameSchema, subPath('propertyNames'))
})
consume('propertyNames', 'object', 'boolean')
}
if (typeof node.additionalProperties === 'object' && typeof node.propertyNames !== 'object') {
enforceValidation('wild-card additionalProperties requires propertyNames')
}
if (Array.isArray(node.required)) {
for (const req of node.required) {
const prop = currPropImm(req)
errorIf('!(%s)', [present(prop)], 'is required', buildName(prop))
}
consume('required', 'array')
}
const dependencies = node.dependencies === undefined ? 'dependentRequired' : 'dependencies'
if (node[dependencies]) {
for (const key of Object.keys(node[dependencies])) {
let deps = node[dependencies][key]
if (typeof deps === 'string') deps = [deps]
const exists = (k) => present(currPropImm(k))
const item = currPropImm(key)
if (Array.isArray(deps)) {
const condition = safeand(...deps.map(exists))
errorIf('%s && !(%s)', [present(item), condition], 'dependencies not set')
} else if (typeof deps === 'object' || typeof deps === 'boolean') {
fun.block('if (%s) {', [present(item)], '}', () => {
rule(current, deps, subPath(dependencies, key))
})
} else {
fail('Unexpected dependencies entry')
}
}
consume(dependencies, 'object')
}
if (typeof node.properties === 'object') {
for (const p of Object.keys(node.properties)) {
rule(currPropImm(p), node.properties[p], subPath('properties', p))
}
consume('properties', 'object')
}
if (node.patternProperties) {
const key = gensym('key')
fun.block('for (const %s of Object.keys(%s)) {', [key, name], '}', () => {
for (const p of Object.keys(node.patternProperties)) {
enforceRegex(p, node.propertyNames || {})
fun.block('if (%s.test(%s)) {', [patterns(p), key], '}', () => {
const sub = currPropVar(key, true) // always own property, from Object.keys
rule(sub, node.patternProperties[p], subPath('patternProperties', p))
})
}
})
consume('patternProperties', 'object')
}
if (node.additionalProperties || node.additionalProperties === false) {
const key = gensym('key')
const toCompare = (p) => format('%s !== %j', key, p)
const toTest = (p) => format('!%s.test(%s)', patterns(p), key)
const additionalProp = safeand(
...Object.keys(node.properties || {}).map(toCompare),
...Object.keys(node.patternProperties || {}).map(toTest)
)
fun.block('for (const %s of Object.keys(%s)) {', [key, name], '}', () => {
fun.block('if (%s) {', [additionalProp], '}', () => {
if (node.additionalProperties === false) {
if (removeAdditional) {
fun.write('delete %s[%s]', name, key)
} else {
error('has additional properties', null, format('%j + %s', `${name}.`, key))
}
} else {
const sub = currPropVar(key, true) // always own property, from Object.keys
rule(sub, node.additionalProperties, subPath('additionalProperties'))
}
})
})
consume('additionalProperties', 'object', 'boolean')
} else if (typeApplicable('object') && !hasSubValidation) {
enforceValidation('additionalProperties rule must be specified')
}
}
const checkConst = () => {
if (node.const !== undefined) {
const complex = typeof node.const === 'object'
const compare = makeCompare(name, complex)
errorIf('!%s', [compare(node.const)], 'must be const value')
consume('const', 'jsonval')
return true
} else if (node.enum) {
enforce(Array.isArray(node.enum), 'Invalid enum')
const complex = node.enum.some((e) => typeof e === 'object')
const compare = makeCompare(name, complex)
errorIf('!(%s)', [safeor(...node.enum.map(compare))], 'must be an enum value')
consume('enum', 'array')
return true
}
return false
}
const checkGeneric = () => {
if (node.not || node.not === false) {
const prev = gensym('prev')
fun.write('const %s = errors', prev)
subrule(current, node.not, subPath('not'))
fun.write('if (%s === errors) {', prev)
error('negative schema matches')
fun.write('} else errors = %s', prev)
consume('not', 'object', 'boolean')
}
const thenOrElse = node.then || node.then === false || node.else || node.else === false
if ((node.if || node.if === false) && thenOrElse) {
const prev = gensym('prev')
fun.write('const %s = errors', prev)
subrule(current, node.if, subPath('if'))
fun.write('if (%s !== errors) {', prev)
fun.write('errors = %s', prev)
if (node.else || node.else === false) {
rule(current, node.else, subPath('else'))
consume('else', 'object', 'boolean')
}
if (node.then || node.then === false) {
fun.write('} else {')
rule(current, node.then, subPath('then'))
consume('then', 'object', 'boolean')
}
fun.write('}')
consume('if', 'object', 'boolean')
}
if (node.allOf !== undefined) {
enforce(Array.isArray(node.allOf), 'Invalid allOf')
node.allOf.forEach((sch, key) => {
rule(current, sch, subPath('allOf', key))
})
consume('allOf', 'array')
}
if (node.anyOf !== undefined) {
enforce(Array.isArray(node.anyOf), 'Invalid anyOf')
const prev = gensym('prev')
node.anyOf.forEach((sch, i) => {
if (i === 0) {
fun.write('const %s = errors', prev)
} else {
fun.write('if (errors !== %s) {', prev)
fun.write('errors = %s', prev)
}
subrule(current, sch, schemaPath)
})
node.anyOf.forEach((sch, i) => {
if (i > 0) fun.write('}')
})
fun.write('if (%s !== errors) {', prev)
fun.write('errors = %s', prev)
error('no schemas match')
fun.write('}')
consume('anyOf', 'array')
}
if (node.oneOf !== undefined) {
enforce(Array.isArray(node.oneOf), 'Invalid oneOf')
const prev = gensym('prev')
const passes = gensym('passes')
fun.write('const %s = errors', prev)
fun.write('let %s = 0', passes)
for (const sch of node.oneOf) {
subrule(current, sch, schemaPath)
fun.write('if (%s === errors) { %s++ } else errors = %s', prev, passes, prev)
}
errorIf('%s !== 1', [passes], 'no (or more than one) schemas match')
consume('oneOf', 'array')
}
}
const maybeWrap = (shouldWrap, fmt, args, close, writeBody) => {
if (!shouldWrap) return writeBody()
fun.block(fmt, args, close, writeBody)
}
const typeWrap = (checkBlock, validTypes, queryType) => {
const [funSize, unusedSize] = [fun.size(), unused.size]
const alwaysValidType = typeArray && typeArray.every((type) => validTypes.includes(type))
maybeWrap(!alwaysValidType, 'if (%s) {', [queryType], '}', checkBlock)
// enforce check that non-applicable blocks are empty and no rules were applied
if (funSize !== fun.size() || unusedSize !== unused.size)
enforce(typeApplicable(...validTypes), `Unexpected rules in type`, node.type)
}
/* Actual post-$ref validation happens here */
const needTypeValidation = typeArray !== null
if (needTypeValidation) {
const typeValidate = safeor(...typeArray.map((t) => types.get(t)(name)))
errorIf('!(%s)', [typeValidate], 'is the wrong type')
}
if (node.type !== undefined) consume('type', 'string', 'array')
// If type validation was needed and did not return early, wrap this inside an else clause.
maybeWrap(needTypeValidation && allErrors, 'else {', [], '}', () => {
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])
return
}
typeWrap(checkNumbers, ['number', 'integer'], types.get('number')(name))
typeWrap(checkStrings, ['string'], types.get('string')(name))
typeWrap(checkArrays, ['array'], types.get('array')(name))
typeWrap(checkObjects, ['object'], types.get('object')(name))
checkGeneric()
})
finish()
if (opts.includeErrors) {
fun.write('if (!deepEqual(data, JSON.parse(JSON.stringify(data)))) {')
fun.write('validate.errors = [{schemaPath:"#",dataPath:"#",message:"not JSON compatible"}]')
fun.write('return false')
fun.write('}')
fun.write('const res = actualValidate(data)')
fun.write('validate.errors = actualValidate.errors')
fun.write('return res')
} else {
fun.write('return deepEqual(data, JSON.parse(JSON.stringify(data))) && actualValidate(data)')
}
visit(optAllErrors, optIncludeErrors, [], { name: safe('data') }, schema, [])
fun.write('return errors === 0')
fun.write('}')
if (dryRun) return
const validate = fun.makeFunction(scope)

@@ -844,4 +39,2 @@ validate.toModule = () => fun.makeModule(scope)

const validator = (schema, opts = {}) => compile(schema, schema, opts, Object.create(null))
const parser = function(schema, opts = {}) {

@@ -856,7 +49,7 @@ // strong mode is default in parser

if (validate(data)) return data
const message = validate.errors
? validate.errors.map((err) => `${err.field} ${err.message}`).join('\n')
: ''
const error = new Error(`JSON validation error${message ? `: ${message}` : ''}`)
error.errors = validate.errors
const reason = validate.errors ? validate.errors[0] : null
const keyword = reason && reason.schemaPath ? reason.schemaPath.replace(/.*\//, '') : '??'
const explanation = reason ? ` for ${keyword} at ${reason.dataPath}` : ''
const error = new Error(`JSON validation failed${explanation}`)
if (validate.errors) error.errors = validate.errors
throw error

@@ -863,0 +56,0 @@ }

@@ -17,3 +17,3 @@ 'use strict'

if (Object.getPrototypeOf(item) !== Function.prototype)
throw new Error('Can not stringify a function with unexpected prototype')
throw new Error('Can not stringify: a function with unexpected prototype')

@@ -29,11 +29,11 @@ const stringified = `${item}`

// Shortened ES6 object method declaration
throw new Error('Can stringify only either normal or arrow functions')
throw new Error('Can not stringify: only either normal or arrow functions are supported')
} else if (typeof item === 'object') {
const proto = Object.getPrototypeOf(item)
if (item instanceof RegExp && proto === RegExp.prototype) return format('%r', item)
throw new Error('Can not stringify an object with unexpected prototype')
throw new Error('Can not stringify: an object with unexpected prototype')
}
throw new Error(`Cannot stringify ${item} - unknown type ${typeof item}`)
throw new Error(`Can not stringify: unknown type ${typeof item}`)
}
module.exports = jaystring
'use strict'
module.exports = [
'$schema', // version
...['id', '$id', '$ref', 'definitions', '$defs'], // pointers
...['$schema', '$vocabulary'], // version
...['id', '$id', '$anchor', '$ref', 'definitions', '$defs', '$recursiveRef', '$recursiveAnchor'], // pointers
...['type', 'required', 'default'], // generic

@@ -14,3 +14,3 @@ ...['enum', 'const'], // constant values

...['properties', 'maxProperties', 'minProperties', 'additionalProperties', 'patternProperties'], // objects
...['propertyNames', 'dependencies', 'dependentRequired'], // objects
...['propertyNames', 'dependencies', 'dependentRequired', 'dependentSchemas'], // objects
// Unused meta keywords not affecting validation (annotations and comments)

@@ -17,0 +17,0 @@ // https://json-schema.org/understanding-json-schema/reference/generic.html

'use strict'
function toPointer(path) {
if (path.length === 0) return '#'
return `#/${path.map((part) => `${part}`.replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}`
}
function untilde(string) {

@@ -31,2 +26,3 @@ if (!string.includes('~')) return string

if (typeof part !== 'string') throw new Error('Invalid JSON pointer')
if (objpath) objpath.push(curr) // does not include target itself, but includes head
const prop = untilde(part)

@@ -36,5 +32,3 @@ if (typeof curr !== 'object') return undefined

curr = curr[prop]
if (objpath) objpath.push(curr)
}
if (objpath) objpath.pop() // does not include head or result
return curr

@@ -58,8 +52,10 @@ }

const ids = objpath.map((obj) => (obj && (obj.$id || obj.id)) || '')
return ids.filter((id) => id).reduce(joinPath, '')
return ids.filter((id) => id && typeof id === 'string').reduce(joinPath, '')
}
// Returns a list of resolved entries, in a form: [schema, root, basePath]
// basePath doesn't contain the target object $id itself
function resolveReference(root, additionalSchemas, ref, base = '') {
const ptr = joinPath(base, ref)
const schemas = new Map(Object.entries(additionalSchemas))
const schemas = new Map(additionalSchemas)
const self = (base || '').split('#')[0]

@@ -81,9 +77,15 @@ if (self) schemas.set(self, root)

if (path === ptr || (path === main && local === '')) {
results.push([sub, root, ptr])
results.push([sub, root, oldPath])
} else if (path === main && local[0] === '/') {
const objpath = []
const res = get(sub, local, objpath)
if (res !== undefined) results.push([res, root, joinPath(path, objpath2path(objpath))])
if (res !== undefined) results.push([res, root, joinPath(oldPath, objpath2path(objpath))])
}
}
if (sub.$anchor && typeof sub.$anchor === 'string') {
if (sub.$anchor.includes('#')) throw new Error("$anchor can't include '#'")
if (sub.$anchor.startsWith('/')) throw new Error("$anchor can't start with '/'")
path = joinPath(path, `#${sub.$anchor}`)
if (path === ptr) results.push([sub, root, oldPath])
}
for (const k of Object.keys(sub)) visit(sub[k], path)

@@ -110,2 +112,36 @@ }

module.exports = { toPointer, get, joinPath, resolveReference }
const buildSchemas = (input) => {
if (input) {
switch (Object.getPrototypeOf(input)) {
case Object.prototype:
return new Map(Object.entries(input))
case Map.prototype:
return new Map(input)
case Array.prototype: {
// In this case, schema ids are extracted from the schemas themselves
const schemas = new Map()
const cleanId = (id) =>
// # is allowed only as the last symbol here
id && typeof id === 'string' && !/#./.test(id) ? id.replace(/#$/, '') : null
for (const schema of input) {
const visit = (sub) => {
if (!sub || typeof sub !== 'object') return
const id = cleanId(sub.$id || sub.id)
if (id && id.includes('://')) {
if (schemas.has(id)) throw new Error("Duplicate schema $id in 'schemas'")
schemas.set(id, sub)
} else if (sub === schema) {
throw new Error("Schema with missing or invalid $id in 'schemas'")
}
for (const k of Object.keys(sub)) visit(sub[k])
}
visit(schema)
}
return schemas
}
}
}
throw new Error("Unexpected value for 'schemas' option")
}
module.exports = { get, joinPath, resolveReference, buildSchemas }

@@ -12,3 +12,10 @@ 'use strict'

const jsval = (val) => {
if ([Infinity, -Infinity, NaN, undefined].includes(val)) return `${val}`
if ([Infinity, -Infinity, NaN, undefined, null].includes(val)) return `${val}`
const primitive = ['string', 'boolean', 'number'].includes(typeof val)
if (!primitive) {
if (typeof val !== 'object') throw new Error('Unexpected value type')
const proto = Object.getPrototypeOf(val)
const ok = (proto === Array.prototype && Array.isArray(val)) || proto === Object.prototype
if (!ok) throw new Error('Unexpected object given as value')
}
return (

@@ -62,3 +69,3 @@ JSON.stringify(val)

const safe = (string) => {
if (!/^[a-z][a-z0-9]*$/.test(string)) throw new Error('Does not look like a safe id')
if (!/^[a-z][a-z0-9_]*$/i.test(string)) throw new Error('Does not look like a safe id')
return new SafeString(string)

@@ -65,0 +72,0 @@ }

@@ -5,22 +5,13 @@ 'use strict'

// https://mathiasbynens.be/notes/javascript-unicode#accounting-for-astral-symbols
const stringLength = (string) => [...string].length
const stringLength = (string) =>
/[\uD800-\uDFFF]/.test(string) ? [...string].length : string.length
const isMultipleOf = (value, multipleOf) => {
if (typeof multipleOf !== 'number' || !Number.isFinite(value))
throw new Error('multipleOf is not a number')
if (typeof value !== 'number' || !Number.isFinite(value)) return false
if (value === 0) return true
if (multipleOf === 0) return false
const digitsAfterDot = (number) => {
if ((number | 0) === number) return 0
return String(number)
.split('.')
.pop().length
}
const digits = digitsAfterDot(multipleOf)
if (digits === 0) return value % multipleOf === 0
const valueDigits = digitsAfterDot(value)
if (valueDigits > digits) return false
const factor = Math.pow(10, digits)
return Math.round(factor * value) % Math.round(factor * multipleOf) === 0
// A isMultipleOf B: shortest decimal denoted as A % shortest decimal denoted as B === 0
// Optimized, sanity checks and precomputation are outside of this method
const isMultipleOf = (value, divisor, factor, factorMultiple) => {
if (value % divisor === 0) return true
const multiple = value * factor
if (multiple % factorMultiple === 0) return true
const normal = Math.floor(multiple + 0.5)
return normal / factor === value && normal % factorMultiple === 0
}

@@ -72,2 +63,13 @@

module.exports = { stringLength, isMultipleOf, deepEqual, unique, hasOwn }
// Used for error generation. Affects error performance, optimized
const pointerPart = (s) => (/~\//.test(s) ? `${s}`.replace(/~/g, '~0').replace(/\//g, '~1') : s)
const toPointer = (path) => (path.length === 0 ? '#' : `#/${path.map(pointerPart).join('/')}`)
const errorMerge = ({ schemaPath, dataPath, ...more }, schemaBase, dataBase) => ({
schemaPath: `${schemaBase}${schemaPath.slice(1)}`,
dataPath: `${dataBase}${dataPath.slice(1)}`,
...more,
})
const errorUtils = { toPointer, pointerPart, errorMerge }
module.exports = { stringLength, isMultipleOf, deepEqual, unique, hasOwn, ...errorUtils }
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc