Skip to content

Commit

Permalink
refactor: split validate code generation
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Aug 25, 2020
1 parent 7f03fdd commit 0aad959
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 131 deletions.
18 changes: 17 additions & 1 deletion lib/compile/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ enum Block {
If,
Else,
For,
Func,
}

type Code = string | (() => void)
export type Code = string | (() => void)

export default class CodeGen {
#names: {[key: string]: number} = {}
Expand Down Expand Up @@ -108,6 +109,21 @@ export default class CodeGen {
return this
}

func(name = "", args = "", async?: boolean, funcBody?: Code): CodeGen {
this.#blocks.push(Block.Func)
this.code(`${async ? "async " : ""}function ${name}(${args}){`)
if (funcBody) this.code(funcBody).endFunc()
return this
}

endFunc(): CodeGen {
const b = this._lastBlock
if (b !== Block.Func) throw new Error('CodeGen: "endFunc" without "func"')
this.#blocks.pop()
this.code(`}`)
return this
}

get _lastBlock(): Block {
return this.#blocks[this._last()]
}
Expand Down
6 changes: 2 additions & 4 deletions lib/compile/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import CodeGen from "./codegen"
import {toQuotedString} from "./util"
import {quotedString} from "../vocabularies/util"
import {validateCode} from "./validate"
import {validateFunctionCode} from "./validate"
import {validateKeywordSchema} from "./validate/keyword"
import {ErrorObject, KeywordCompilationResult} from "../types"

Expand Down Expand Up @@ -109,9 +109,8 @@ function compile(schema, root, localRefs, baseId) {

const gen = new CodeGen()
// TODO refactor to extract code from gen
validateCode({
validateFunctionCode({
allErrors: !!opts.allErrors,
isTop: true,
topSchemaRef: "validate.schema",
async: _schema.$async === true,
schema: _schema,
Expand All @@ -123,7 +122,6 @@ function compile(schema, root, localRefs, baseId) {
errSchemaPath: "#",
errorPath: '""',
dataPathArr: [""],
level: 0,
dataLevel: 0,
data: "data", // TODO get unique name when passed from applicator keywords
gen,
Expand Down
6 changes: 3 additions & 3 deletions lib/compile/subschema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {CompilationContext} from "../types"
import {validateCode} from "./validate"
import {subschemaCode} from "./validate"
import {getProperty, escapeFragment, getPath, getPathExpr} from "./util"
import {quotedString} from "../vocabularies/util"

Expand Down Expand Up @@ -47,8 +47,8 @@ export function applySubschema(
const subschema = getSubschema(it, appl)
extendSubschemaData(subschema, it, appl)
extendSubschemaMode(subschema, appl)
const nextContext = {...it, ...subschema, level: it.level + 1}
validateCode(nextContext, valid)
const nextContext = {...it, ...subschema}
subschemaCode(nextContext, valid)
}

function getSubschema(
Expand Down
37 changes: 17 additions & 20 deletions lib/compile/validate/boolSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,24 @@ const boolError: KeywordErrorDefinition = {
params: () => "{}",
}

export function booleanOrEmptySchema(it: CompilationContext, valid: string): void {
const {gen, isTop, schema} = it
if (isTop) {
if (schema === false) {
falseSchemaError(it, false)
} else if (schema.$async === true) {
gen.code("return data;")
} else {
gen.code("validate.errors = null; return true;")
}
gen.code(
`};
return validate;`
)
export function topBoolOrEmptySchema(it: CompilationContext): void {
const {gen, schema} = it
if (schema === false) {
falseSchemaError(it, false)
} else if (schema.$async === true) {
gen.code("return data;")
} else {
if (schema === false) {
gen.code(`var ${valid} = false;`) // TODO var
falseSchemaError(it)
} else {
gen.code(`var ${valid} = true;`) // TODO var
}
gen.code("validate.errors = null; return true;")
}
}

export function boolOrEmptySchema(it: CompilationContext, valid: string): void {
const {gen, schema} = it
if (schema === false) {
gen.code(`var ${valid} = false;`) // TODO var
falseSchemaError(it)
} else {
gen.code(`var ${valid} = true;`) // TODO var
}
}

Expand Down
172 changes: 75 additions & 97 deletions lib/compile/validate/index.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,72 @@
import {CompilationContext} from "../../types"
import {schemaUnknownRules, schemaHasRules, schemaHasRulesExcept} from "../util"
import {quotedString} from "../../vocabularies/util"
import {booleanOrEmptySchema} from "./boolSchema"
import {topBoolOrEmptySchema, boolOrEmptySchema} from "./boolSchema"
import {getSchemaTypes, coerceAndCheckDataType} from "./dataType"
import {schemaKeywords} from "./iterate"
import CodeGen, {Code} from "../codegen"

const resolve = require("../resolve")

// schema compilation (render) time:
// this function is used recursively to generate code for sub-schemas
//
// runtime:
// "validate" is a variable name to which this function will be assigned
// validateRef etc. are defined in the parent scope in index.js
export function validateCode(it: CompilationContext, valid?: string): string | void {
const {
isTop,
schema,
level,
gen,
opts: {$comment},
} = it

// TODO valid must be non-optional or maybe it must be returned
if (!valid) valid = `valid${level}`

// schema compilation - generates validation function, subschemaCode (below) is used for subschemas
export function validateFunctionCode(it: CompilationContext): void {
const {schema, opts} = it
checkKeywords(it)

if (isTop) startFunction(it)
if (booleanOrEmpty(it, valid)) return
if ($comment && schema.$comment) commentKeyword(it)

if (isTop) {
delete it.isTop
if (isBoolOrEmpty(it)) {
validateFunction(it, () => topBoolOrEmptySchema(it))
return
}
validateFunction(it, () => {
if (opts.$comment && schema.$comment) commentKeyword(it)
checkNoDefault(it)
initializeTop(it)
initializeTop(it.gen)
typeAndKeywords(it)
endFunction(it)
} else {
updateContext(it)
checkAsync(it)
const errsCount = gen.name("_errs")
// TODO var - async validation fails, possibly because of nodent
gen.code(`var ${errsCount} = errors;`)
typeAndKeywords(it, errsCount)
// TODO var, level
gen.code(`var ${valid} = ${errsCount} === errors;`)
returnResults(it)
})
}

function validateFunction(it: CompilationContext, body: Code) {
const {gen} = it
gen.func("validate", "data, dataPath, parentData, parentDataProperty, rootData", it.async, body)
gen.code(
`"use strict";
${funcSourceUrl(it)}`
)
gen.code(`return validate;`)
}

function funcSourceUrl({schema, opts}: CompilationContext): string {
return schema.$id && (opts.sourceCode || opts.processCode)
? `/*# sourceURL=${schema.$id as string} */`
: ""
}

// schema compilation - this function is used recursively to generate code for sub-schemas
export function subschemaCode(it: CompilationContext, valid: string): void {
const {schema, gen, opts} = it
checkKeywords(it)
if (isBoolOrEmpty(it)) {
boolOrEmptySchema(it, valid)
return
}
if (opts.$comment && schema.$comment) commentKeyword(it)
updateContext(it)
checkAsync(it)
const errsCount = gen.name("_errs")
// TODO var - async validation fails if var replaced, possibly because of nodent
gen.code(`var ${errsCount} = errors;`)
typeAndKeywords(it, errsCount)
// TODO var
gen.code(`var ${valid} = ${errsCount} === errors;`)
}

function checkKeywords(it: CompilationContext) {
checkUnknownKeywords(it)
checkRefsAndKeywords(it)
}

function booleanOrEmpty(it: CompilationContext, valid: string): true | void {
const {schema, RULES} = it
if (typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)) {
booleanOrEmptySchema(it, valid)
return true
}
function isBoolOrEmpty({schema, RULES}: CompilationContext): boolean {
return typeof schema == "boolean" || !schemaHasRules(schema, RULES.all)
}

function typeAndKeywords(it: CompilationContext, errsCount?: string): void {
Expand All @@ -68,17 +75,12 @@ function typeAndKeywords(it: CompilationContext, errsCount?: string): void {
schemaKeywords(it, types, !checkedTypes, errsCount)
}

function checkUnknownKeywords({
schema,
RULES,
opts: {strictKeywords},
logger,
}: CompilationContext): void {
if (strictKeywords) {
function checkUnknownKeywords({schema, RULES, opts, logger}: CompilationContext): void {
if (opts.strictKeywords) {
const unknownKeyword = schemaUnknownRules(schema, RULES.keywords)
if (unknownKeyword) {
const msg = `unknown keyword: "${unknownKeyword}"`
if (strictKeywords === "log") logger.warn(msg)
if (opts.strictKeywords === "log") logger.warn(msg)
else throw new Error(msg)
}
}
Expand All @@ -88,53 +90,33 @@ function checkRefsAndKeywords({
schema,
errSchemaPath,
RULES,
opts: {extendRefs},
opts,
logger,
}: CompilationContext): void {
if (schema.$ref && schemaHasRulesExcept(schema, RULES.all, "$ref")) {
if (extendRefs === "fail") {
if (opts.extendRefs === "fail") {
throw new Error(`$ref: sibling validation keywords at "${errSchemaPath}" (option extendRefs)`)
} else if (extendRefs !== true) {
} else if (opts.extendRefs !== true) {
logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`)
}
}
}

function startFunction({
gen,
schema,
async,
opts: {sourceCode, processCode},
}: CompilationContext): void {
const asyncFunc = async ? "async" : ""
const sourceUrl =
schema.$id && (sourceCode || processCode) ? `/*# sourceURL=${schema.$id as string} */` : ""
gen.code(
`const validate = ${asyncFunc} function(data, dataPath, parentData, parentDataProperty, rootData) {
'use strict';
${sourceUrl}`
)
}

function checkNoDefault({
schema,
opts: {useDefaults, strictDefaults},
logger,
}: CompilationContext): void {
if (schema.default !== undefined && useDefaults && strictDefaults) {
function checkNoDefault({schema, opts, logger}: CompilationContext): void {
if (schema.default !== undefined && opts.useDefaults && opts.strictDefaults) {
const msg = "default is ignored in the schema root"
if (strictDefaults === "log") logger.warn(msg)
if (opts.strictDefaults === "log") logger.warn(msg)
else throw new Error(msg)
}
}

function initializeTop({gen}: CompilationContext): void {
// TODO old comment: "don't edit, used in replace". Should be removed?
gen.code(
`let vErrors = null;
let errors = 0;
if (rootData === undefined) rootData = data;`
)
function initializeTop(gen: CodeGen): void {
gen
.code(
`let vErrors = null;
let errors = 0;`
)
.if(`rootData === undefined`, `rootData = data;`)
}

function updateContext(it: CompilationContext): void {
Expand All @@ -155,17 +137,13 @@ function commentKeyword({gen, schema, errSchemaPath, opts: {$comment}}: Compilat
}
}

function endFunction({gen, async}: CompilationContext) {
// TODO old comment: "don't edit, used in replace". Should be removed?
gen.code(
async
? `if (errors === 0) return data;
else throw new ValidationError(vErrors);`
: `validate.errors = vErrors;
return errors === 0;`
)
gen.code(
`};
return validate;`
)
function returnResults({gen, async}: CompilationContext) {
if (async) {
gen.if("errors === 0", "return data", "throw new ValidationError(vErrors)")
} else {
gen.code(
`validate.errors = vErrors;
return errors === 0;`
)
}
}
5 changes: 1 addition & 4 deletions lib/keyword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,7 @@ function ruleCode(it: CompilationContext, keyword: string, ruleType?: string): v
}

function ok(condition?: string): void {
if (!allErrors) {
if (condition) gen.if(condition)
else gen.if("true")
}
if (!allErrors) gen.if(condition || "true")
}

function errorParams(obj: KeywordContextParams, assign?: true) {
Expand Down
2 changes: 0 additions & 2 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ export type KeywordCompilationResult = object | boolean | SchemaValidateFunction

export interface CompilationContext {
allErrors: boolean
level: number
dataLevel: number
data: string
dataPathArr: (string | number)[]
Expand Down Expand Up @@ -133,7 +132,6 @@ export interface CompilationContext {
self: any // TODO
RULES: ValidationRules
logger: Logger // TODO ?
isTop?: boolean // TODO ?
root: SchemaRoot // TODO ?
rootId: string // TODO ?
topSchemaRef: string
Expand Down

0 comments on commit 0aad959

Please sign in to comment.