@exodus/schemasafe
Advanced tools
Comparing version 1.0.0-rc.3 to 1.0.0-rc.4
168
index.d.ts
@@ -1,39 +0,153 @@ | ||
// This typings are experimental and known to be incomplete. | ||
// These typings are experimental and known to be incomplete. | ||
// Help wanted at https://github.com/ExodusMovement/schemasafe/issues/130 | ||
type Json = string | number | boolean | null | Array<Json> | { [id: string]: Json } | ||
type Schema = | ||
| true | ||
| false | ||
| { | ||
// version | ||
$schema?: string | ||
$vocabulary?: string | ||
// pointers | ||
id?: string | ||
$id?: string | ||
$anchor?: string | ||
$ref?: string | ||
definitions?: { [id: string]: Schema } | ||
$defs?: { [id: string]: Schema } | ||
$recursiveRef?: string | ||
$recursiveAnchor?: boolean | ||
// generic | ||
type?: string | Array<string> | ||
required?: Array<string> | ||
default?: Json | ||
// constant values | ||
enum?: Array<Json> | ||
const?: Json | ||
// logical checks | ||
not?: Schema | ||
allOf?: Array<Schema> | ||
anyOf?: Array<Schema> | ||
oneOf?: Array<Schema> | ||
if?: Schema | ||
then?: Schema | ||
else?: Schema | ||
// numbers | ||
maximum?: number | ||
minimum?: number | ||
exclusiveMaximum?: number | boolean | ||
exclusiveMinimum?: number | boolean | ||
multipleOf?: number | ||
divisibleBy?: number | ||
// arrays, basic | ||
items?: Schema | Array<Schema> | ||
maxItems?: number | ||
minItems?: number | ||
additionalItems?: Schema | ||
// arrays, complex | ||
contains?: Schema | ||
minContains?: number | ||
maxContains?: number | ||
uniqueItems?: boolean | ||
// strings | ||
maxLength?: number | ||
minLength?: number | ||
format?: string | ||
pattern?: string | ||
// strings content | ||
contentEncoding?: string | ||
contentMediaType?: string | ||
contentSchema?: Schema | ||
// objects | ||
properties?: { [id: string]: Schema } | ||
maxProperties?: number | ||
minProperties?: number | ||
additionalProperties?: Schema | ||
patternProperties?: { [pattern: string]: Schema } | ||
propertyNames?: Schema | ||
dependencies?: { [id: string]: Array<string> | Schema } | ||
dependentRequired?: { [id: string]: Array<string> } | ||
dependentSchemas?: { [id: string]: Schema } | ||
// see-through | ||
unevaluatedProperties?: Schema | ||
unevaluatedItems?: Schema | ||
// Unused meta keywords not affecting validation (annotations and comments) | ||
// https://json-schema.org/understanding-json-schema/reference/generic.html | ||
// https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9 | ||
title?: string | ||
description?: string | ||
deprecated?: boolean | ||
readOnly?: boolean | ||
writeOnly?: boolean | ||
examples?: Array<Json> | ||
$comment?: string | ||
// optimization hint and error filtering only, does not affect validation result | ||
discriminator?: { propertyName: string; mapping?: { [value: string]: string } } | ||
} | ||
interface ValidationError { | ||
keywordLocation: string; | ||
instanceLocation: string; | ||
keywordLocation: string | ||
instanceLocation: string | ||
} | ||
interface Validate { | ||
(value: any): boolean; | ||
errors?: ValidationError[]; | ||
(value: Json): boolean | ||
errors?: ValidationError[] | ||
toModule(): string | ||
toJSON(): Schema | ||
} | ||
interface ValidatorOptions { | ||
mode?: string, | ||
useDefaults?: boolean; | ||
removeAdditional?: boolean; | ||
includeErrors?: boolean; | ||
allErrors?: boolean; | ||
dryRun?: boolean; | ||
allowUnusedKeywords?: boolean; | ||
allowUnreachable?: boolean; | ||
requireSchema?: boolean; | ||
requireValidation?: boolean; | ||
requireStringValidation?: boolean; | ||
forbidNoopValues?: boolean; | ||
complexityChecks?: boolean; | ||
unmodifiedPrototypes?: boolean; | ||
isJSON?: boolean; | ||
$schemaDefault?: string | null; | ||
formats?: any; // FIXME | ||
weakFormats?: boolean; | ||
extraFormats?: boolean; | ||
schemas?: any; // FIXME | ||
mode?: string | ||
useDefaults?: boolean | ||
removeAdditional?: boolean | ||
includeErrors?: boolean | ||
allErrors?: boolean | ||
dryRun?: boolean | ||
allowUnusedKeywords?: boolean | ||
allowUnreachable?: boolean | ||
requireSchema?: boolean | ||
requireValidation?: boolean | ||
requireStringValidation?: boolean | ||
forbidNoopValues?: boolean | ||
complexityChecks?: boolean | ||
unmodifiedPrototypes?: boolean | ||
isJSON?: boolean | ||
jsonCheck?: boolean | ||
$schemaDefault?: string | null | ||
formats?: { [key: string]: RegExp | ((input: string) => boolean) } | ||
weakFormats?: boolean | ||
extraFormats?: boolean | ||
schemas?: Map<string, Schema> | Array<Schema> | { [id: string]: Schema } | ||
} | ||
declare const validator: (schema: object, options?: ValidatorOptions) => Validate; | ||
interface ParseResult { | ||
valid: boolean | ||
value?: Json | ||
error?: string | ||
errors?: ValidationError[] | ||
} | ||
export { validator, Validate, ValidationError, ValidatorOptions }; | ||
interface Parse { | ||
(value: string): ParseResult | ||
toModule(): string | ||
toJSON(): Schema | ||
} | ||
declare const validator: (schema: Schema, options?: ValidatorOptions) => Validate | ||
declare const parser: (schema: Schema, options?: ValidatorOptions) => Parse | ||
export { | ||
validator, | ||
parser, | ||
Validate, | ||
ValidationError, | ||
ValidatorOptions, | ||
ParseResult, | ||
Parse, | ||
Json, | ||
Schema, | ||
} |
{ | ||
"name": "@exodus/schemasafe", | ||
"version": "1.0.0-rc.3", | ||
"version": "1.0.0-rc.4", | ||
"description": "JSON Safe Parser & Schema Validator", | ||
@@ -31,3 +31,3 @@ "license": "MIT", | ||
"scripts": { | ||
"lint": "prettier --list-different '**/*.js'&& eslint .", | ||
"lint": "prettier --list-different '**/*.js' && eslint .", | ||
"format": "prettier --write '**/*.js'", | ||
@@ -56,2 +56,5 @@ "coverage": "c8 --reporter=lcov --reporter=text npm run test", | ||
}, | ||
"resolutions": { | ||
"tap-spec/tap-out/trim": "^1.0.1" | ||
}, | ||
"keywords": [ | ||
@@ -58,0 +61,0 @@ "JSON", |
@@ -60,3 +60,3 @@ # `@exodus/schemasafe` | ||
Or use the [parser mode](./doc/Parser-not-validator.md) (running in | ||
Or use the [parser API](./doc/Parser-not-validator.md) (running in | ||
[strong mode](./doc/Strong-mode.md) by default): | ||
@@ -80,6 +80,9 @@ | ||
console.log('returns { valid: true, value }:', parse('{"hello": "world" }')) | ||
console.log('returns { valid: false }:', parse('{}')) | ||
console.log(parse('{"hello": "world" }')) // { valid: true, value: { hello: 'world' } } | ||
console.log(parse('{}')) // { valid: false } | ||
``` | ||
Parser API is recommended, because this way you can avoid handling unvalidated JSON objects in | ||
non-string form at all in your code. | ||
## Options | ||
@@ -92,3 +95,3 @@ | ||
`@exodus/schemasafe` supports the formats specified in JSON schema v4 (such as date-time). | ||
If you want to add your own custom formats pass them as the formats options to the validator | ||
If you want to add your own custom formats pass them as the formats options to the validator: | ||
@@ -98,11 +101,22 @@ ```js | ||
type: 'string', | ||
format: 'no-foo' | ||
}, { | ||
formats: { | ||
'no-foo': (str) => !str.includes('foo'), | ||
} | ||
}) | ||
console.log(validate('test')) // true | ||
console.log(validate('foo')) // false | ||
const parse = parser({ | ||
$schema: 'https://json-schema.org/draft/2019-09/schema', | ||
type: 'string', | ||
format: 'only-a' | ||
}, { | ||
formats: { | ||
'only-a': /^a+$/ | ||
'only-a': /^a+$/, | ||
} | ||
}) | ||
console.log(validate('aa')) // true | ||
console.log(validate('ab')) // false | ||
console.log(parse('"aa"')) // { valid: true, value: 'aa' } | ||
console.log(parse('"ab"')) // { valid: false } | ||
``` | ||
@@ -159,2 +173,26 @@ | ||
Or, similarly, with parser API: | ||
```js | ||
const schema = { | ||
$schema: 'https://json-schema.org/draft/2019-09/schema', | ||
type: 'object', | ||
required: ['hello'], | ||
properties: { | ||
hello: { | ||
type: 'string', | ||
pattern: '^[a-z]+$', | ||
} | ||
}, | ||
additionalProperties: false, | ||
} | ||
const parse = parser(schema, { includeErrors: true }) | ||
console.log(parse('{ "hello": 100 }')); | ||
// { valid: false, | ||
// error: 'JSON validation failed for type at #/hello', | ||
// errors: [ { keywordLocation: '#/properties/hello/type', instanceLocation: '#/hello' } ] | ||
// } | ||
``` | ||
Only the first error is reported by default unless `allErrors` option is also set to `true` in | ||
@@ -209,2 +247,16 @@ addition to `includeErrors`. | ||
## Contributing | ||
Get a fully set up development environment with: | ||
```sh | ||
git clone https://github.com/ExodusMovement/schemasafe | ||
cd schemasafe | ||
git submodule update --init --recursive | ||
yarn | ||
yarn lint | ||
yarn test | ||
``` | ||
## Previous work | ||
@@ -220,5 +272,5 @@ | ||
versions. | ||
## License | ||
[MIT](./LICENSE) | ||
@@ -47,2 +47,13 @@ 'use strict' | ||
const rootMeta = new WeakMap() | ||
const generateMeta = (root, $schema, enforce, requireSchema) => { | ||
if ($schema) { | ||
const version = $schema.replace(/^http:\/\//, 'https://').replace(/#$/, '') | ||
enforce(schemaVersions.includes(version), 'Unexpected schema version:', version) | ||
rootMeta.set(root, { exclusiveRefs: schemaIsOlderThan(version, 'draft/2019-09') }) | ||
} else { | ||
enforce(!requireSchema, '[requireSchema] $schema is required') | ||
rootMeta.set(root, {}) | ||
} | ||
} | ||
const compileSchema = (schema, root, opts, scope, basePathRoot = '') => { | ||
@@ -144,3 +155,3 @@ const { | ||
const recursiveAnchor = schema && schema.$recursiveAnchor === true | ||
const getMeta = () => rootMeta.get(root) || {} | ||
const getMeta = () => rootMeta.get(root) | ||
const basePathStack = basePathRoot ? [basePathRoot] : [] | ||
@@ -194,2 +205,3 @@ const visit = (errors, history, current, node, schemaPath, trace = {}, { constProp } = {}) => { | ||
const complex = (msg, arg) => enforce(!complexityChecks, `[complexityChecks] ${msg}`, arg) | ||
const saveMeta = ($sch) => generateMeta(root, $sch || $schemaDefault, enforce, requireSchema) | ||
@@ -243,8 +255,3 @@ // evaluated tracing | ||
if (node === root) { | ||
const $schema = get('$schema', 'string') || $schemaDefault | ||
if ($schema) { | ||
const version = $schema.replace(/^http:\/\//, 'https://').replace(/#$/, '') | ||
enforce(schemaVersions.includes(version), 'Unexpected schema version:', version) | ||
rootMeta.set(root, { exclusiveRefs: schemaIsOlderThan(version, 'draft/2019-09') }) | ||
} else enforce(!requireSchema, '[requireSchema] $schema is required') | ||
saveMeta(get('$schema', 'string')) | ||
handle('$vocabulary', ['object'], ($vocabulary) => { | ||
@@ -257,3 +264,3 @@ for (const [vocab, flag] of Object.entries($vocabulary)) { | ||
}) | ||
} | ||
} else if (!getMeta()) saveMeta(root.$schema) | ||
@@ -354,3 +361,4 @@ handle('examples', ['array'], null) // unused, meta-only | ||
// Those checks will need to be skipped if another error is set in this block before those ones | ||
const haveComplex = node.uniqueItems || node.pattern || node.patternProperties || node.format | ||
const havePattern = node.pattern && !noopRegExps.has(node.pattern) // we won't generate code for noop | ||
const haveComplex = node.uniqueItems || havePattern || node.patternProperties || node.format | ||
const prev = allErrors && haveComplex ? gensym('prev') : null | ||
@@ -625,5 +633,6 @@ const prevWrap = (shouldWrap, writeBody) => | ||
handle('propertyNames', ['object', 'boolean'], (names) => { | ||
handle('propertyNames', ['object', 'boolean'], (s) => { | ||
forObjectKeys(current, (sub, key) => { | ||
const nameSchema = typeof names === 'object' ? { type: 'string', ...names } : names | ||
// Add default type for non-ref schemas, so strong mode is fine with omitting it | ||
const nameSchema = typeof s === 'object' && !s.$ref ? { type: 'string', ...s } : s | ||
const nameprop = Object.freeze({ name: key, errorParent: sub, type: 'string' }) | ||
@@ -710,4 +719,5 @@ rule(nameprop, nameSchema, subPath('propertyNames')) | ||
const checkConst = () => { | ||
if (handle('const', ['jsonval'], (val) => safenot(compare(name, val)))) return true | ||
return handle('enum', ['array'], (vals) => { | ||
const handledConst = handle('const', ['jsonval'], (val) => safenot(compare(name, val))) | ||
if (handledConst && !allowUnusedKeywords) return true // enum can't be present, this is rechecked by allowUnusedKeywords | ||
const handledEnum = handle('enum', ['array'], (vals) => { | ||
const objects = vals.filter((value) => value && typeof value === 'object') | ||
@@ -717,2 +727,3 @@ const primitive = vals.filter((value) => !(value && typeof value === 'object')) | ||
}) | ||
return handledConst || handledEnum | ||
} | ||
@@ -922,7 +933,10 @@ | ||
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]) | ||
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 | ||
if (!allowUnusedKeywords) { | ||
// 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]) | ||
// If it does though, we should not short-circuit validation. This could be optimized by extracting types, but not significant | ||
return | ||
} | ||
} | ||
@@ -955,2 +969,12 @@ | ||
if (!sub && sub !== false) fail('failed to resolve $ref:', $ref) | ||
if (sub.type) { | ||
// This could be done better, but for now we check only the direct type in the $ref | ||
const type = Array.isArray(sub.type) ? sub.type : [sub.type] | ||
evaluateDelta({ type }) | ||
if (requireValidation) { | ||
// If validation is required, then $ref is guranteed to validate all items and properties | ||
if (type.includes('array')) evaluateDelta({ items: Infinity }) | ||
if (type.includes('object')) evaluateDelta({ properties: [true] }) | ||
} | ||
} | ||
const n = getref(sub) || compileSchema(sub, subRoot, opts, scope, path) | ||
@@ -957,0 +981,0 @@ return applyRef(n, { path: ['$ref'] }) |
@@ -57,3 +57,3 @@ 'use strict' | ||
} catch ({ message }) { | ||
return { valid: false, message } | ||
return { valid: false, error: message } | ||
} | ||
@@ -79,2 +79,3 @@ } | ||
].join('\n') | ||
parse.toJSON = () => schema | ||
return parse | ||
@@ -81,0 +82,0 @@ } |
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
113716
2050
270