Skip to content

Commit

Permalink
refactor: validate to typescript (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Aug 13, 2020
1 parent 3337b28 commit b2b257b
Show file tree
Hide file tree
Showing 20 changed files with 762 additions and 142 deletions.
17 changes: 17 additions & 0 deletions lib/compile/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export default class CodeGen {
#names: {[key: string]: number} = {}
// TODO make private. Possibly stack?
_out = ""

name(prefix: string): string {
if (!this.#names[prefix]) this.#names[prefix] = 0
const num = this.#names[prefix]++
return `${prefix}_${num}`
}

code(str: string): CodeGen {
// TODO optionally strip whitespace
this._out += str + "\n"
return this
}
}
52 changes: 52 additions & 0 deletions lib/compile/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {KeywordContext, KeywordErrorDefinition} from "../types"
import {toQuotedString} from "./util"

export function reportError(
cxt: KeywordContext,
error: KeywordErrorDefinition,
allErrors?: boolean
): void {
const {gen, compositeRule, opts, async} = cxt.it
const errObj = errorObjectCode(cxt, error)
if (allErrors ?? (compositeRule || opts.allErrors)) {
gen.code(
`const err = ${errObj};
if (vErrors === null) vErrors = [err];
else vErrors.push(err);
errors++;`
)
} else {
gen.code(
async
? `throw new ValidationError([${errObj}]);`
: `validate.errors = [${errObj}];
return false;`
)
}
}

function errorObjectCode(cxt: KeywordContext, error: KeywordErrorDefinition): string {
const {
keyword,
data,
schemaValue,
it: {createErrors, schemaPath, errorPath, errSchemaPath, opts},
} = cxt
if (createErrors === false) return "{}"
if (!error) throw new Error('keyword definition must have "error" property')
// TODO trim whitespace
let out = `{
keyword: "${keyword}",
dataPath: (dataPath || "") + ${errorPath},
schemaPath: ${toQuotedString(errSchemaPath + "/" + keyword)},
params: ${error.params(cxt)},`
if (opts.messages !== false) out += `message: ${error.message(cxt)},`
if (opts.verbose) {
// TODO trim whitespace
out += `
schema: ${schemaValue},
parentSchema: validate.schema${schemaPath},
data: ${data},`
}
return out + "}"
}
4 changes: 2 additions & 2 deletions lib/compile/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Scope from "./scope"
import CodeGen from "./codegen"
import {toQuotedString} from "./util"
import {MissingRefError} from "./error_classes"
const equal = require("fast-deep-equal")
Expand Down Expand Up @@ -94,7 +94,7 @@ function compile(schema, root, localRefs, baseId) {
schemaPath: "",
errSchemaPath: "#",
errorPath: '""',
scope: new Scope(),
gen: new CodeGen(),
MissingRefError,
RULES: RULES,
validate: validateGenerator,
Expand Down
13 changes: 0 additions & 13 deletions lib/compile/scope.ts

This file was deleted.

13 changes: 9 additions & 4 deletions lib/compile/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export function checkDataType(
}
}

export function checkDataTypes(dataTypes: string[], data: string, strictNumbers?: boolean): string {
export function checkDataTypes(
dataTypes: string[],
data: string,
strictNumbers?: boolean,
negate?: true
): string {
if (dataTypes.length === 1) {
return checkDataType(dataTypes[0], data, strictNumbers, true)
}
Expand All @@ -62,7 +67,7 @@ export function checkDataTypes(dataTypes: string[], data: string, strictNumbers?
}
if (types.number) delete types.integer
for (const t in types) {
code += (code ? " && " : "") + checkDataType(t, data, strictNumbers, true)
code += (code ? " && " : "") + checkDataType(t, data, strictNumbers, negate)
}
return code
}
Expand Down Expand Up @@ -173,7 +178,7 @@ export function getPath(currentPath: string, prop: string, jsonPointers?: boolea

const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/
const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/
export function getData($data: string, lvl: number, paths: string[]): string {
export function getData($data: string, lvl: number, paths: (string | undefined)[]): string {
let jsonPointer, data
if ($data === "") return "rootData"
if ($data[0] === "/") {
Expand All @@ -193,7 +198,7 @@ export function getData($data: string, lvl: number, paths: string[]): string {
"Cannot access property/index " + up + " levels up, current level is " + lvl
)
}
return paths[lvl - up]
return paths[lvl - up] || ""
}

if (up > lvl) {
Expand Down
18 changes: 18 additions & 0 deletions lib/compile/validate/applicability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {CompilationContext} from "../../types"

export function schemaHasRulesForType({RULES, schema}: CompilationContext, ty: string) {
const group = RULES.types[ty]
return group && group !== true && shouldUseGroup(schema, group)
}

function shouldUseGroup(schema, rulesGroup): boolean {
return rulesGroup.some((rule) => shouldUseRule(schema, rule))
}

function shouldUseRule(schema, rule): boolean {
return schema[rule.keyword] !== undefined || ruleImplementsSomeKeyword(schema, rule)
}

function ruleImplementsSomeKeyword(schema, rule): boolean {
return rule.implements && rule.implements.some((kwd) => schema[kwd] !== undefined)
}
105 changes: 105 additions & 0 deletions lib/compile/validate/boolSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {KeywordErrorDefinition, CompilationContext, KeywordContext} from "../../types"
import {reportError} from "../errors"

const boolError: KeywordErrorDefinition = {
message: () => '"boolean schema is false"',
params: () => "{}",
}

export function booleanOrEmptySchema(it: CompilationContext): void {
const {gen, isTop, schema, level} = 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;`
)
} else {
if (schema === false) {
gen.code(`var valid${level} = false;`) // TODO level, var
falseSchemaError(it)
} else {
gen.code(`var valid${level} = true;`) // TODO level, var
}
}

// if (schema === false) {
// if (!isTop) {
// gen.code(`var valid${level} = false;`) // TODO level, var
// }
// // TODO probably some other interface should be used for non-keyword validation errors...
// falseSchemaError(it, !isTop)
// } else {
// if (isTop) {
// gen.code(schema.$async === true ? `return data;` : `validate.errors = null; return true;`)
// } else {
// gen.code(`var valid${level} = true;`) // TODO level, var
// }
// }
// if (isTop) {
// gen.code(
// `};
// return validate;`
// )
// }
}

function falseSchemaError(it: CompilationContext, allErrors?: boolean) {
const {gen, dataLevel} = it
// TODO maybe some other interface should be used for non-keyword validation errors...
const cxt: KeywordContext = {
gen,
fail: exception,
ok: exception,
errorParams: exception,
keyword: "false schema",
data: "data" + (dataLevel || ""),
$data: false,
schema: false,
schemaCode: false,
schemaValue: false,
parentSchema: false,
it,
}
reportError(cxt, boolError, allErrors)
}

function exception() {
throw new Error("this function can only be used in keyword")
}

// {{ var $keyword = 'false schema'; }}
// {{# def.setupKeyword }}
// {{? it.schema === false}}
// {{? it.isTop}}
// {{ $breakOnError = true; }}
// {{??}}
// var {{=$valid}} = false;
// {{?}}
// {{# def.error:'false schema' }}
// {{??}}
// {{? it.isTop}}
// {{? $async }}
// return data;
// {{??}}
// validate.errors = null;
// return true;
// {{?}}
// {{??}}
// var {{=$valid}} = true;
// {{?}}
// {{?}}

// {{? it.isTop}}
// };
// return validate;
// {{?}}

// {{ return out; }}
// {{?}}
50 changes: 50 additions & 0 deletions lib/compile/validate/dataType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {CompilationContext} from "../../types"
import {toHash, checkDataTypes} from "../util"
import {schemaHasRulesForType} from "./applicability"

export function getSchemaTypes({schema, opts}: CompilationContext): string[] {
const t = schema.type
const types: string[] = Array.isArray(t) ? t : t || []
types.forEach(checkType)
if (opts.nullable) {
const hasNull = types.includes("null")
if (hasNull && schema.nullable === false) {
throw new Error('{"type": "null"} contradicts {"nullable": "false"}')
} else if (!hasNull && schema.nullable === true) {
types.push("null")
}
}
return types

function checkType(t: string): void {
// TODO check that type is allowed
if (typeof t != "string") throw new Error('"type" keyword must be string or string[]')
}
}

export function coerceAndCheckDataType(it: CompilationContext, types: string[]): void {
const {
gen,
dataLevel,
opts: {coerceTypes, strictNumbers},
} = it
let coerceTo = coerceToTypes(types, coerceTypes)
if (coerceTo.length || types.length > 1 || !schemaHasRulesForType(it, types[0])) {
const wrongType = checkDataTypes(types, `data${dataLevel || ""}`, strictNumbers, true)
gen.code(`if (${wrongType}) {`)
if (coerceTo.length) coerceType(it)
else reportTypeError(it)
gen.code("}")
}
}

function coerceType(_: CompilationContext) {}

function reportTypeError(_: CompilationContext) {}

const COERCIBLE = toHash(["string", "number", "integer", "boolean", "null"])
function coerceToTypes(types: string[], coerceTypes?: boolean | "array"): string[] {
return coerceTypes
? types.filter((t) => COERCIBLE[t] || (coerceTypes === "array" && t === "array"))
: []
}
Loading

0 comments on commit b2b257b

Please sign in to comment.