Skip to content

Commit

Permalink
feat: support schemaType in all keywords, including $data; report $da…
Browse files Browse the repository at this point in the history
…ta error if $data schema validation fails
  • Loading branch information
epoberezkin committed Aug 31, 2020
1 parent b1befd9 commit 6210e4a
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 86 deletions.
9 changes: 7 additions & 2 deletions lib/compile/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ export function _(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code {
return new _Code(strs.reduce((res, s, i) => res + interpolate(args[i - 1]) + s))
}

export function str(strs: TemplateStringsArray, ...args: TemplateArg[]): _Code {
export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[])[]): _Code {
return new _Code(
strs.map(safeStringify).reduce((res, s, i) => {
let aStr = interpolate(args[i - 1])
let aStr = interpolateStr(args[i - 1])
if (aStr instanceof _Code && aStr.isQuoted()) aStr = aStr.toString()
return typeof aStr === "string"
? res.slice(0, -1) + aStr.slice(1, -1) + s.slice(1)
Expand All @@ -130,6 +130,11 @@ function interpolate(x: TemplateArg): TemplateArg {
: safeStringify(x)
}

function interpolateStr(x: TemplateArg | string[]): TemplateArg {
if (Array.isArray(x)) x = x.join(",")
return interpolate(x)
}

export interface CodeGenOptions {
es5?: boolean
lines?: boolean
Expand Down
12 changes: 10 additions & 2 deletions lib/compile/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import {
} from "../types"
import {schemaRefOrVal} from "../vocabularies/util"
import {getData} from "./util"
import {reportError, reportExtraError, resetErrorsCount, keywordError} from "./errors"
import {
reportError,
reportExtraError,
resetErrorsCount,
keywordError,
keyword$DataError,
} from "./errors"
import CodeGen, {Code, Name} from "./codegen"
import N from "./names"

Expand All @@ -19,6 +25,7 @@ export default class KeywordContext implements KeywordErrorContext {
schema: any
schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value
schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data)
schemaType?: string | string[]
parentSchema: any
errsCount?: Name
params: KeywordContextParams
Expand All @@ -34,6 +41,7 @@ export default class KeywordContext implements KeywordErrorContext {
this.schema = it.schema[keyword]
this.$data = def.$data && it.opts.$data && this.schema && this.schema.$data
this.schemaValue = schemaRefOrVal(it, this.schema, keyword, this.$data)
this.schemaType = def.schemaType
this.parentSchema = it.schema
this.params = {}
this.it = it
Expand Down Expand Up @@ -88,7 +96,7 @@ export default class KeywordContext implements KeywordErrorContext {
}

$dataError(): void {
reportError(this, this.def.$dataError || this.def.error || keywordError)
reportError(this, this.def.$dataError || keyword$DataError)
}

reset(): void {
Expand Down
10 changes: 8 additions & 2 deletions lib/compile/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import CodeGen, {_, str, Code, Name} from "./codegen"
import N from "./names"

export const keywordError: KeywordErrorDefinition = {
message: ({keyword}) => str`should pass ${keyword} keyword validation`,
params: ({keyword}) => _`{keyword: ${keyword}}`, // TODO possibly remove it as keyword is reported in the object
message: ({keyword}) => str`should pass "${keyword}" keyword validation`,
}

export const keyword$DataError: KeywordErrorDefinition = {
message: ({keyword, schemaType}) =>
schemaType
? str`"${keyword}" keyword must be ${schemaType} ($data)`
: str`"${keyword}" keyword is invalid ($data)`,
}

export function reportError(
Expand Down
6 changes: 3 additions & 3 deletions lib/compile/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function checkDataType(
cond = _`Array.isArray(${data})`
break
case "object":
cond = _`${data} && typeof ${data} === "object" && !Array.isArray(${data})`
cond = _`${data} && typeof ${data} == "object" && !Array.isArray(${data})`
break
case "integer":
cond = numCond(_`!(${data} % 1) && !isNaN(${data})`)
Expand All @@ -36,7 +36,7 @@ export function checkDataType(
return correct === DataType.Correct ? cond : _`!(${cond})`

function numCond(_cond: Code = nil): Code {
return and(_`typeof ${data} === "number"`, _cond, strictNumbers ? _`isFinite(${data})` : nil)
return and(_`typeof ${data} == "number"`, _cond, strictNumbers ? _`isFinite(${data})` : nil)
}
}

Expand All @@ -52,7 +52,7 @@ export function checkDataTypes(
let cond: Code
const types = toHash(dataTypes)
if (types.array && types.object) {
const notObj = _`typeof ${data} !== "object"`
const notObj = _`typeof ${data} != "object"`
cond = types.null ? notObj : _`(!${data} || ${notObj})`
delete types.null
delete types.array
Expand Down
29 changes: 14 additions & 15 deletions lib/compile/validate/dataType.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {CompilationContext, KeywordErrorDefinition, KeywordErrorContext} from "../../types"
import {toHash, checkDataType, checkDataTypes, DataType} from "../util"
import {toHash, checkDataTypes, DataType} from "../util"
import {schemaRefOrVal} from "../../vocabularies/util"
import {schemaHasRulesForType} from "./applicability"
import {reportError} from "../errors"
import {_, str, Name} from "../codegen"

export function getSchemaTypes({schema, opts, RULES}: CompilationContext): string[] {
export function getSchemaTypes({opts, RULES}: CompilationContext, schema): string[] {
const st: undefined | string | string[] = schema.type
const types: string[] = Array.isArray(st) ? st : st ? [st] : []
types.forEach(checkType)
Expand Down Expand Up @@ -34,7 +34,7 @@ export function coerceAndCheckDataType(it: CompilationContext, types: string[]):
if (checkTypes) {
const wrongType = checkDataTypes(types, data, opts.strictNumbers, DataType.Wrong)
gen.if(wrongType, () => {
if (coerceTo.length) coerceData(it, coerceTo)
if (coerceTo.length) coerceData(it, types, coerceTo)
else reportTypeError(it)
})
}
Expand All @@ -48,16 +48,16 @@ function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string
: []
}

export function coerceData(it: CompilationContext, coerceTo: string[]): void {
const {gen, schema, data, opts} = it
function coerceData(it: CompilationContext, types: string[], coerceTo: string[]): void {
const {gen, data, opts} = it
const dataType = gen.let("dataType", _`typeof ${data}`)
const coerced = gen.let("coerced", _`undefined`)
if (opts.coerceTypes === "array") {
gen.if(_`${dataType} == 'object' && Array.isArray(${data}) && ${data}.length == 1`, () =>
gen
.assign(data, _`${data}[0]`)
.assign(dataType, _`typeof ${data}`)
.if(checkDataType(schema.type, data, opts.strictNumbers), () => gen.assign(coerced, data))
.if(checkDataTypes(types, data, opts.strictNumbers), () => gen.assign(coerced, data))
)
}
gen.if(_`${coerced} !== undefined`)
Expand Down Expand Up @@ -134,25 +134,24 @@ function assignParentData(
}

const typeError: KeywordErrorDefinition = {
message: ({schema}) =>
str`should be ${Array.isArray(schema) ? schema.join(",") : <string>schema}`,
// TODO change: return type as array here
params: ({schema}) => _`{type: ${Array.isArray(schema) ? schema.join(",") : <string>schema}}`,
message: ({schema}) => str`should be ${schema}`,
params: ({schema, schemaValue}) =>
typeof schema == "string" ? _`{type: ${schema}}` : _`{type: ${schemaValue}}`,
}

export function reportTypeError(it: CompilationContext): void {
const cxt = getErrorContext(it, "type")
const cxt = getTypeErrorContext(it)
reportError(cxt, typeError)
}

function getErrorContext(it: CompilationContext, keyword: string): KeywordErrorContext {
function getTypeErrorContext(it: CompilationContext): KeywordErrorContext {
const {gen, data, schema} = it
const schemaCode = schemaRefOrVal(it, schema, keyword)
const schemaCode = schemaRefOrVal(it, schema, "type")
return {
gen,
keyword,
keyword: "type",
data,
schema: schema[keyword],
schema: schema.type,
schemaCode,
schemaValue: schemaCode,
parentSchema: schema,
Expand Down
2 changes: 1 addition & 1 deletion lib/compile/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function isBoolOrEmpty({schema, RULES}: CompilationContext): boolean {
}

function typeAndKeywords(it: CompilationContext, errsCount?: Name): void {
const types = getSchemaTypes(it)
const types = getSchemaTypes(it, it.schema)
const checkedTypes = coerceAndCheckDataType(it, types)
schemaKeywords(it, types, !checkedTypes, errsCount)
}
Expand Down
3 changes: 1 addition & 2 deletions lib/compile/validate/iterate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ export function schemaKeywords(

function groupKeywords(group: RuleGroup): void {
if (group.type) {
const checkType = checkDataType(group.type, data, opts.strictNumbers)
gen.if(checkType)
gen.if(checkDataType(group.type, data, opts.strictNumbers))
iterateKeywords(it, group)
if (types.length === 1 && types[0] === group.type && typeErrors) {
gen.else()
Expand Down
74 changes: 35 additions & 39 deletions lib/compile/validate/keyword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import {
import KeywordContext from "../context"
import {applySubschema} from "../subschema"
import {extendErrors} from "../errors"
import {checkDataTypes, DataType} from "../util"
import {callValidateCode} from "../../vocabularies/util"
import CodeGen, {_, nil, Code, Name} from "../codegen"
import CodeGen, {_, nil, or, Code, Name} from "../codegen"
import N from "../names"

export function keywordCode(
Expand Down Expand Up @@ -52,54 +53,55 @@ function macroKeywordCode(cxt: KeywordContext, def: MacroKeywordDefinition) {
}

function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) {
const {gen, keyword, schema, schemaCode, parentSchema, $data, it} = cxt
const {gen, keyword, schema, schemaCode, schemaType, parentSchema, $data, it} = cxt
checkAsync(it, def)
const validate =
"compile" in def && !$data ? def.compile.call(it.self, schema, parentSchema, it) : def.validate
const validateRef = useKeyword(gen, keyword, validate)
const valid = gen.let("valid")

if (def.errors === false) {
validateNoErrorsRule()
} else {
validateRuleWithErrors()
}
gen.block(def.errors === false ? validateNoErrorsRule : validateRuleWithErrors)
cxt.ok(def.valid ?? valid)

function validateNoErrorsRule(): void {
gen.block(() => {
if ($data) check$data()
assignValid()
if (def.modifying) modifyData(cxt)
})
if (!def.valid) cxt.pass(valid)
if ($data) check$data()
assignValid()
if (def.modifying) modifyData(cxt)
reportKeywordErrors(() => cxt.error())
}

function validateRuleWithErrors(): void {
gen.block()
if ($data) check$data()
// const errsCount = gen.const("_errs", N.errors)
const ruleErrs = def.async ? validateAsyncRule() : validateSyncRule()
if (def.modifying) modifyData(cxt)
gen.endBlock()
reportKeywordErrors(ruleErrs)
reportKeywordErrors(() => addKeywordErrors(cxt, ruleErrs))
}

function check$data(): void {
gen
// TODO add support for schemaType in keyword definition
// .if(`${bad$DataType(schemaCode, <string>def.schemaType, $data)} false`) // TODO refactor
.if(_`${schemaCode} === undefined`)
.assign(valid, true)
.else()
gen.if(_`${schemaCode} === undefined`).assign(valid, true)
if (schemaType || def.validateSchema) {
gen.elseIf(or(wrong$DataType(), invalid$DataSchema()))
cxt.$dataError()
gen.assign(valid, false)
}
gen.else()
}

function wrong$DataType(): Code {
if (schemaType) {
if (!(schemaCode instanceof Name)) throw new Error("ajv implementation error")
const st = Array.isArray(schemaType) ? schemaType : [schemaType]
return _`(${checkDataTypes(st, schemaCode, it.opts.strictNumbers, DataType.Wrong)})`
}
return nil
}

function invalid$DataSchema(): Code {
if (def.validateSchema) {
const validateSchemaRef = useKeyword(gen, keyword, def.validateSchema)
gen.assign(valid, _`${validateSchemaRef}(${schemaCode})`)
// TODO fail if schema fails validation
// gen.if(`!${valid}`)
// reportError(cxt, keywordError)
// gen.else()
gen.if(valid)
return _`!${validateSchemaRef}(${schemaCode})`
}
return nil
}

function validateAsyncRule(): Name {
Expand Down Expand Up @@ -129,16 +131,10 @@ function funcKeywordCode(cxt: KeywordContext, def: FuncKeywordDefinition) {
gen.assign(valid, _`${await}${callValidateCode(cxt, validateRef, passCxt, passSchema)}`)
}

function reportKeywordErrors(ruleErrs: Code): void {
switch (def.valid) {
case true:
return
case false:
addKeywordErrors(cxt, ruleErrs)
return cxt.ok(false) // TODO maybe add gen.skip() to remove code till the end of the block?
default:
cxt.pass(valid, () => addKeywordErrors(cxt, ruleErrs))
}
// TODO maybe refactor to gen.ifNot(def.valid ?? valid, repErrs) once dead branches are removed
function reportKeywordErrors(repErrs: () => void): void {
if (def.valid === false) repErrs()
else if (def.valid !== true) gen.ifNot(valid, repErrs)
}
}

Expand Down
1 change: 1 addition & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export interface KeywordErrorContext {
parentSchema: any
schemaCode: Code | number | boolean
schemaValue: Code | number | boolean
schemaType?: string | string[]
errsCount?: Name
params: KeywordContextParams
it: CompilationContext
Expand Down
2 changes: 1 addition & 1 deletion lib/vocabularies/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function bad$DataType(
schemaType: string,
$data?: string | false
): Code {
return $data ? _`(${schemaCode}!==undefined && typeof ${schemaCode}!==${schemaType})` : nil
return $data ? _`(${schemaCode} !== undefined && typeof ${schemaCode} != ${schemaType})` : nil
}

export function schemaRefOrVal(
Expand Down
3 changes: 0 additions & 3 deletions lib/vocabularies/validation/required.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,6 @@ const def: CodeKeywordDefinition = {
str`should have required property '${missingProperty}'`,
params: ({params: {missingProperty}}) => _`{missingProperty: ${missingProperty}}`,
},
$dataError: {
message: '"required" keyword value must be array',
},
}

module.exports = def
Loading

0 comments on commit 6210e4a

Please sign in to comment.