From 9e2e6b99380d4d37d6cb5a22a8059aa38f9c1957 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Sep 2020 17:45:59 +0100 Subject: [PATCH] refactor: group all validation function params after data into dataCxt, refactor code generation for object literals --- lib/compile/codegen/code.ts | 16 +++++++++++ lib/compile/codegen/index.ts | 9 ++++++- lib/compile/errors.ts | 43 ++++++++++++++++++++---------- lib/compile/index.ts | 4 +-- lib/compile/names.ts | 1 + lib/compile/validate/index.ts | 50 ++++++++++++++++++++++++----------- lib/types/index.ts | 28 ++++++++------------ lib/vocabularies/util.ts | 17 +++++++----- spec/keyword.spec.ts | 2 +- 9 files changed, 112 insertions(+), 58 deletions(-) diff --git a/lib/compile/codegen/code.ts b/lib/compile/codegen/code.ts index 746e874b0..8ddee43aa 100644 --- a/lib/compile/codegen/code.ts +++ b/lib/compile/codegen/code.ts @@ -14,6 +14,10 @@ export class _Code { return len >= 2 && this._str[0] === '"' && this._str[len - 1] === '"' } + emptyStr(): boolean { + return this._str === "" || this._str === '""' + } + add(c: _Code): void { this._str += c._str } @@ -31,6 +35,10 @@ export class Name extends _Code { return false } + emptyStr(): boolean { + return false + } + add(_c: _Code): void { throw new Error("CodeGen: can't add to Name") } @@ -66,6 +74,10 @@ export function str(strs: TemplateStringsArray, ...args: (TemplateArg | string[] ) } +export function strConcat(c1: Code, c2: Code): Code { + return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}` +} + function interpolate(x: TemplateArg): TemplateArg { return x instanceof _Code || typeof x == "number" || typeof x == "boolean" || x === null ? x @@ -90,3 +102,7 @@ function safeStringify(x: unknown): string { export function getProperty(key: Code | string | number): Code { return typeof key == "string" && IDENTIFIER.test(key) ? new _Code(`.${key}`) : _`[${key}]` } + +export function keyValue(key: Name, value: SafeExpr, es5?: boolean): Code { + return key === value && !es5 ? key : _`${key}: ${value}` +} diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index 117795bf2..485fc1ecd 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -2,7 +2,7 @@ import type {ScopeValueSets, NameValue, ValueScope, ValueScopeName} from "./scop import {_, nil, _Code, Code, Name} from "./code" import {Scope} from "./scope" -export {_, str, nil, getProperty, stringify, Name, Code} from "./code" +export {_, str, strConcat, nil, getProperty, stringify, Name, Code} from "./code" export {Scope, ScopeStore, ValueScope} from "./scope" enum BlockKind { @@ -116,6 +116,13 @@ export class CodeGen { return this } + object(...keyValues: [Name, SafeExpr][]): _Code { + const values = keyValues + .map(([key, value]) => (key === value && !this.opts.es5 ? key : `${key}: ${value}`)) + .reduce((c1, c2) => `${c1},${c2}`) + return new _Code(`{${values}}`) + } + if(condition: Code | boolean, thenBody?: Block, elseBody?: Block): CodeGen { this._blocks.push(BlockKind.If) this._out += `if(${condition}){` + this._n diff --git a/lib/compile/errors.ts b/lib/compile/errors.ts index 0ccd54845..2a6ca4e6f 100644 --- a/lib/compile/errors.ts +++ b/lib/compile/errors.ts @@ -1,5 +1,6 @@ import type {KeywordErrorCxt, KeywordErrorDefinition, SchemaCxt} from "../types" -import {CodeGen, _, str, Code, Name} from "./codegen" +import {CodeGen, _, str, strConcat, Code, Name} from "./codegen" +import {SafeExpr} from "./codegen/code" import N from "./names" export const keywordError: KeywordErrorDefinition = { @@ -59,7 +60,7 @@ export function extendErrors({ gen.const(err, _`${N.vErrors}[${i}]`) gen.if( _`${err}.dataPath === undefined`, - _`${err}.dataPath = (${N.dataPath} || '') + ${it.errorPath}` + _`${err}.dataPath = ${strConcat(N.dataPath, it.errorPath)}` ) gen.code(_`${err}.schemaPath = ${str`${it.errSchemaPath}/${keyword}`}`) if (it.opts.verbose) { @@ -84,28 +85,42 @@ function returnErrors(it: SchemaCxt, errs: Code): void { } } +const E = { + keyword: new Name("keyword"), + schemaPath: new Name("schemaPath"), + params: new Name("params"), + propertyName: new Name("propertyName"), + message: new Name("message"), + schema: new Name("schema"), + parentSchema: new Name("parentSchema"), +} + function errorObjectCode(cxt: KeywordErrorCxt, error: KeywordErrorDefinition): Code { const { keyword, data, schemaValue, - it: {createErrors, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts}, + it: {gen, createErrors, topSchemaRef, schemaPath, errorPath, errSchemaPath, propertyName, opts}, } = cxt if (createErrors === false) return _`{}` const {params, message} = error - const msg = typeof message == "string" ? message : message(cxt) - const par = params ? params(cxt) : _`{}` - const out = _`{keyword: ${keyword}, dataPath: (${N.dataPath} || "") + ${errorPath}` - out.add(_`, schemaPath: ${str`${errSchemaPath}/${keyword}`}, params: ${par}`) - if (propertyName) { - out.add(_`, propertyName: ${propertyName}`) - } + const keyValues: [Name, SafeExpr][] = [ + [E.keyword, _`${keyword}`], + [N.dataPath, strConcat(N.dataPath, errorPath)], + [E.schemaPath, str`${errSchemaPath}/${keyword}`], + [E.params, params ? params(cxt) : _`{}`], + ] + if (propertyName) keyValues.push([E.propertyName, propertyName]) if (opts.messages !== false) { - out.add(_`, message: ${msg}`) + const msg = typeof message == "string" ? _`${message}` : message(cxt) + keyValues.push([E.message, msg]) } if (opts.verbose) { - out.add(_`, schema: ${schemaValue}, parentSchema: ${topSchemaRef}${schemaPath}, data: ${data}`) + keyValues.push( + [E.schema, schemaValue], + [E.parentSchema, _`${topSchemaRef}${schemaPath}`], + [N.data, data] + ) } - out.add(_`}`) - return out + return gen.object(...keyValues) } diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 191539969..5e1bd4592 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -6,7 +6,7 @@ import type { SchemaCxt, } from "../types" import type Ajv from "../ajv" -import {CodeGen, _, nil, str, Name} from "./codegen" +import {CodeGen, _, nil, Name} from "./codegen" import {ValidationError} from "./error_classes" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" @@ -90,7 +90,7 @@ export function compileSchema(this: Ajv, sch: SchemaEnv): SchemaEnv { baseId: sch.baseId || rootId, schemaPath: nil, errSchemaPath: "#", - errorPath: str``, + errorPath: _`""`, opts: this.opts, self: this, } diff --git a/lib/compile/names.ts b/lib/compile/names.ts index b2942974b..11387cb04 100644 --- a/lib/compile/names.ts +++ b/lib/compile/names.ts @@ -4,6 +4,7 @@ const names = { // validation function arguments data: new Name("data"), // data passed to validation function // args passed from referencing schema + dataCxt: new Name("dataCxt"), dataPath: new Name("dataPath"), parentData: new Name("parentData"), parentDataProperty: new Name("parentDataProperty"), diff --git a/lib/compile/validate/index.ts b/lib/compile/validate/index.ts index 5312b94e2..54390b3bc 100644 --- a/lib/compile/validate/index.ts +++ b/lib/compile/validate/index.ts @@ -3,7 +3,7 @@ import type {InstanceOptions} from "../../ajv" import {boolOrEmptySchema, topBoolOrEmptySchema} from "./boolSchema" import {coerceAndCheckDataType, getSchemaTypes} from "./dataType" import {schemaKeywords} from "./iterate" -import {CodeGen, _, nil, str, Block, Code, Name} from "../codegen" +import {_, nil, str, Block, Code, Name, CodeGen} from "../codegen" import N from "../names" import {resolveUrl} from "../resolve" import {schemaCxtHasRules, schemaHasRulesButRef} from "../util" @@ -26,21 +26,46 @@ function validateFunction( body: Block ): void { gen.return(() => - gen.func( - validateName, - _`${N.data}, ${N.dataPath}, ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}`, - schemaEnv.$async, - () => gen.code(_`"use strict"; ${funcSourceUrl(schema, opts)}`).code(body) - ) + opts.code.es5 + ? gen.func(validateName, _`${N.data}, ${N.dataCxt}`, schemaEnv.$async, () => { + gen.code(_`"use strict"; ${funcSourceUrl(schema, opts)}`) + destructureDataCxtES5(gen) + gen.code(body) + }) + : gen.func( + validateName, + _`${N.data}, {${N.dataPath}="", ${N.parentData}, ${N.parentDataProperty}, ${N.rootData}=${N.data}}={}`, + schemaEnv.$async, + () => gen.code(_`${funcSourceUrl(schema, opts)}`).code(body) + ) + ) +} + +function destructureDataCxtES5(gen: CodeGen): void { + gen.if( + N.dataCxt, + () => { + gen.var(N.dataPath, _`${N.dataCxt}.${N.dataPath}`) + gen.var(N.parentData, _`${N.dataCxt}.${N.parentData}`) + gen.var(N.parentDataProperty, _`${N.dataCxt}.${N.parentDataProperty}`) + gen.var(N.rootData, _`${N.dataCxt}.${N.rootData}`) + }, + () => { + gen.var(N.dataPath, _`""`) + gen.var(N.parentData, _`undefined`) + gen.var(N.parentDataProperty, _`undefined`) + gen.var(N.rootData, N.data) + } ) } function topSchemaObjCode(it: SchemaObjCxt): void { - const {schema, opts} = it + const {schema, opts, gen} = it validateFunction(it, () => { if (opts.$comment && schema.$comment) commentKeyword(it) checkNoDefault(it) - initializeTop(it.gen) + gen.let(N.vErrors, null) + gen.let(N.errors, 0) typeAndKeywords(it) returnResults(it) }) @@ -106,13 +131,6 @@ function checkNoDefault(it: SchemaObjCxt): void { } } -function initializeTop(gen: CodeGen): void { - gen.let(N.vErrors, null) - gen.let(N.errors, 0) - gen.if(_`${N.rootData} === undefined`, () => gen.assign(N.rootData, N.data)) - // gen.if(_`${N.dataPath} === undefined`, () => gen.assign(N.dataPath, _`""`)) // TODO maybe add it -} - function updateContext(it: SchemaObjCxt): void { if (it.schema.$id) it.baseId = resolveUrl(it.baseId, it.schema.$id) } diff --git a/lib/types/index.ts b/lib/types/index.ts index fb01132fb..e35bfada1 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -36,15 +36,15 @@ interface SourceCode { scope: Scope } +interface DataValidationCxt { + dataPath: string + parentData: Record | any[] + parentDataProperty: string | number + rootData: Record | any[] +} + export interface ValidateFunction { - ( - this: Ajv | any, - data: any, - dataPath?: string, - parentData?: Record | any[], - parentDataProperty?: string | number, - rootData?: Record | any[] - ): data is T + (this: Ajv | any, data: any, dataCxt?: DataValidationCxt): data is T errors?: null | ErrorObject[] schema?: AnySchema schemaEnv?: SchemaEnv @@ -140,15 +140,9 @@ export interface DataValidateFunction { } export interface SchemaValidateFunction { - ( - schema: any, - data: any, - parentSchema?: AnySchemaObject, - dataPath?: string, - parentData?: Record | any[], - parentDataProperty?: string | number, - rootData?: Record | any[] - ): boolean | Promise + (schema: any, data: any, parentSchema?: AnySchemaObject, dataCxt?: DataValidationCxt): + | boolean + | Promise errors?: Partial[] } diff --git a/lib/vocabularies/util.ts b/lib/vocabularies/util.ts index fe7759c8b..5179e4fb6 100644 --- a/lib/vocabularies/util.ts +++ b/lib/vocabularies/util.ts @@ -1,7 +1,7 @@ import type {AnySchema, SchemaMap, SchemaCxt, SchemaObjCxt} from "../types" import type KeywordCxt from "../compile/context" import {schemaHasRules} from "../compile/util" -import {CodeGen, _, nil, Code, Name, getProperty} from "../compile/codegen" +import {CodeGen, _, strConcat, nil, Code, Name, getProperty} from "../compile/codegen" import N from "../compile/names" export function schemaRefOrVal( @@ -63,16 +63,19 @@ export function noPropertyInData( } export function callValidateCode( - {schemaCode, data, it}: KeywordCxt, + {schemaCode, data, it: {gen, topSchemaRef, schemaPath, errorPath}, it}: KeywordCxt, func: Code, context: Code, passSchema?: boolean ): Code { - const dataAndSchema = passSchema - ? _`${schemaCode}, ${data}, ${it.topSchemaRef}${it.schemaPath}` - : data - const dataPath = _`(${N.dataPath} || '') + ${it.errorPath}` // TODO refactor other places - const args = _`${dataAndSchema}, ${dataPath}, ${it.parentData}, ${it.parentDataProperty}, ${N.rootData}` + const dataAndSchema = passSchema ? _`${schemaCode}, ${data}, ${topSchemaRef}${schemaPath}` : data + const dataCxt = gen.object( + [N.dataPath, strConcat(N.dataPath, errorPath)], + [N.parentData, it.parentData], + [N.parentDataProperty, it.parentDataProperty], + [N.rootData, N.rootData] + ) + const args = _`${dataAndSchema}, ${dataCxt}` return context !== nil ? _`${func}.call(${context}, ${args})` : _`${func}(${args})` } diff --git a/spec/keyword.spec.ts b/spec/keyword.spec.ts index c4d325789..ea4bd63b8 100644 --- a/spec/keyword.spec.ts +++ b/spec/keyword.spec.ts @@ -1200,7 +1200,7 @@ describe("User-defined keywords", () => { function testModifying(withOption) { const collectionFormat = { - csv: function (data, _dataPath, parentData, parentDataProperty) { + csv: function (data, {parentData, parentDataProperty}) { parentData[parentDataProperty] = data.split(",") return true },