forked from ajv-validator/ajv
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: circular dependencies, closes ajv-validator#1399
- Loading branch information
1 parent
22cbf80
commit 714cbc3
Showing
54 changed files
with
537 additions
and
560 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,282 +1,2 @@ | ||
import type { | ||
AddedKeywordDefinition, | ||
KeywordErrorCxt, | ||
KeywordCxtParams, | ||
AnySchemaObject, | ||
} from "../types" | ||
import {SchemaCxt, SchemaObjCxt} from "./index" | ||
import {JSONType} from "./rules" | ||
import {checkDataTypes, DataType} from "./validate/dataType" | ||
import {schemaRefOrVal, unescapeJsonPointer, mergeEvaluated} from "./util" | ||
import { | ||
ErrorPaths, | ||
reportError, | ||
reportExtraError, | ||
resetErrorsCount, | ||
keyword$DataError, | ||
} from "./errors" | ||
import {CodeGen, _, nil, or, not, getProperty, Code, Name} from "./codegen" | ||
import N from "./names" | ||
import {applySubschema, SubschemaArgs} from "./subschema" | ||
|
||
export default class KeywordCxt implements KeywordErrorCxt { | ||
readonly gen: CodeGen | ||
readonly allErrors?: boolean | ||
readonly keyword: string | ||
readonly data: Name // Name referencing the current level of the data instance | ||
readonly $data?: string | false | ||
schema: any // keyword value in the schema | ||
readonly schemaValue: Code | number | boolean // Code reference to keyword schema value or primitive value | ||
readonly schemaCode: Code | number | boolean // Code reference to resolved schema value (different if schema is $data) | ||
readonly schemaType: JSONType[] // allowed type(s) of keyword value in the schema | ||
readonly parentSchema: AnySchemaObject | ||
readonly errsCount?: Name // Name reference to the number of validation errors collected before this keyword, | ||
// requires option trackErrors in keyword definition | ||
params: KeywordCxtParams // object to pass parameters to error messages from keyword code | ||
readonly it: SchemaObjCxt // schema compilation context (schema is guaranteed to be an object, not boolean) | ||
readonly def: AddedKeywordDefinition | ||
|
||
constructor(it: SchemaObjCxt, def: AddedKeywordDefinition, keyword: string) { | ||
validateKeywordUsage(it, def, keyword) | ||
this.gen = it.gen | ||
this.allErrors = it.allErrors | ||
this.keyword = keyword | ||
this.data = it.data | ||
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 | ||
this.def = def | ||
|
||
if (this.$data) { | ||
this.schemaCode = it.gen.const("vSchema", getData(this.$data, it)) | ||
} else { | ||
this.schemaCode = this.schemaValue | ||
if (!validSchemaType(this.schema, def.schemaType, def.allowUndefined)) { | ||
throw new Error(`${keyword} value must be ${JSON.stringify(def.schemaType)}`) | ||
} | ||
} | ||
|
||
if ("code" in def ? def.trackErrors : def.errors !== false) { | ||
this.errsCount = it.gen.const("_errs", N.errors) | ||
} | ||
} | ||
|
||
result(condition: Code, successAction?: () => void, failAction?: () => void): void { | ||
this.gen.if(not(condition)) | ||
if (failAction) failAction() | ||
else this.error() | ||
if (successAction) { | ||
this.gen.else() | ||
successAction() | ||
if (this.allErrors) this.gen.endIf() | ||
} else { | ||
if (this.allErrors) this.gen.endIf() | ||
else this.gen.else() | ||
} | ||
} | ||
|
||
pass(condition: Code, failAction?: () => void): void { | ||
this.result(condition, undefined, failAction) | ||
} | ||
|
||
fail(condition?: Code): void { | ||
if (condition === undefined) { | ||
this.error() | ||
if (!this.allErrors) this.gen.if(false) // this branch will be removed by gen.optimize | ||
return | ||
} | ||
this.gen.if(condition) | ||
this.error() | ||
if (this.allErrors) this.gen.endIf() | ||
else this.gen.else() | ||
} | ||
|
||
fail$data(condition: Code): void { | ||
if (!this.$data) return this.fail(condition) | ||
const {schemaCode} = this | ||
this.fail(_`${schemaCode} !== undefined && (${or(this.invalid$data(), condition)})`) | ||
} | ||
|
||
error(append?: boolean, errorParams?: KeywordCxtParams, errorPaths?: ErrorPaths): void { | ||
if (errorParams) { | ||
this.setParams(errorParams) | ||
this._error(append, errorPaths) | ||
this.setParams({}) | ||
return | ||
} | ||
this._error(append, errorPaths) | ||
} | ||
|
||
private _error(append?: boolean, errorPaths?: ErrorPaths): void { | ||
;(append ? reportExtraError : reportError)(this, this.def.error, errorPaths) | ||
} | ||
|
||
$dataError(): void { | ||
reportError(this, this.def.$dataError || keyword$DataError) | ||
} | ||
|
||
reset(): void { | ||
if (this.errsCount === undefined) throw new Error('add "trackErrors" to keyword definition') | ||
resetErrorsCount(this.gen, this.errsCount) | ||
} | ||
|
||
ok(cond: Code | boolean): void { | ||
if (!this.allErrors) this.gen.if(cond) | ||
} | ||
|
||
setParams(obj: KeywordCxtParams, assign?: true): void { | ||
if (assign) Object.assign(this.params, obj) | ||
else this.params = obj | ||
} | ||
|
||
block$data(valid: Name, codeBlock: () => void, $dataValid: Code = nil): void { | ||
this.gen.block(() => { | ||
this.check$data(valid, $dataValid) | ||
codeBlock() | ||
}) | ||
} | ||
|
||
check$data(valid: Name = nil, $dataValid: Code = nil): void { | ||
if (!this.$data) return | ||
const {gen, schemaCode, schemaType, def} = this | ||
gen.if(or(_`${schemaCode} === undefined`, $dataValid)) | ||
if (valid !== nil) gen.assign(valid, true) | ||
if (schemaType.length || def.validateSchema) { | ||
gen.elseIf(this.invalid$data()) | ||
this.$dataError() | ||
if (valid !== nil) gen.assign(valid, false) | ||
} | ||
gen.else() | ||
} | ||
|
||
invalid$data(): Code { | ||
const {gen, schemaCode, schemaType, def, it} = this | ||
return or(wrong$DataType(), invalid$DataSchema()) | ||
|
||
function wrong$DataType(): Code { | ||
if (schemaType.length) { | ||
/* istanbul ignore if */ | ||
if (!(schemaCode instanceof Name)) throw new Error("ajv implementation error") | ||
const st = Array.isArray(schemaType) ? schemaType : [schemaType] | ||
return _`${checkDataTypes(st, schemaCode, it.opts.strict, DataType.Wrong)}` | ||
} | ||
return nil | ||
} | ||
|
||
function invalid$DataSchema(): Code { | ||
if (def.validateSchema) { | ||
const validateSchemaRef = gen.scopeValue("validate$data", {ref: def.validateSchema}) // TODO value.code for standalone | ||
return _`!${validateSchemaRef}(${schemaCode})` | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
subschema(appl: SubschemaArgs, valid: Name): SchemaCxt { | ||
return applySubschema(this.it, appl, valid) | ||
} | ||
|
||
mergeEvaluated(schemaCxt: SchemaCxt, toName?: typeof Name): void { | ||
const {it, gen} = this | ||
if (!it.opts.unevaluated) return | ||
if (it.props !== true && schemaCxt.props !== undefined) { | ||
it.props = mergeEvaluated.props(gen, schemaCxt.props, it.props, toName) | ||
} | ||
if (it.items !== true && schemaCxt.items !== undefined) { | ||
it.items = mergeEvaluated.items(gen, schemaCxt.items, it.items, toName) | ||
} | ||
} | ||
|
||
mergeValidEvaluated(schemaCxt: SchemaCxt, valid: Name): boolean | void { | ||
const {it, gen} = this | ||
if (it.opts.unevaluated && (it.props !== true || it.items !== true)) { | ||
gen.if(valid, () => this.mergeEvaluated(schemaCxt, Name)) | ||
return true | ||
} | ||
} | ||
} | ||
|
||
function validSchemaType(schema: unknown, schemaType: JSONType[], allowUndefined = false): boolean { | ||
// TODO add tests | ||
return ( | ||
!schemaType.length || | ||
schemaType.some((st) => | ||
st === "array" | ||
? Array.isArray(schema) | ||
: st === "object" | ||
? schema && typeof schema == "object" && !Array.isArray(schema) | ||
: typeof schema == st || (allowUndefined && typeof schema == "undefined") | ||
) | ||
) | ||
} | ||
|
||
function validateKeywordUsage( | ||
{schema, opts, self}: SchemaObjCxt, | ||
def: AddedKeywordDefinition, | ||
keyword: string | ||
): void { | ||
/* istanbul ignore if */ | ||
if (Array.isArray(def.keyword) ? !def.keyword.includes(keyword) : def.keyword !== keyword) { | ||
throw new Error("ajv implementation error") | ||
} | ||
|
||
const deps = def.dependencies | ||
if (deps?.some((kwd) => !Object.prototype.hasOwnProperty.call(schema, kwd))) { | ||
throw new Error(`parent schema must have dependencies of ${keyword}: ${deps.join(",")}`) | ||
} | ||
|
||
if (def.validateSchema) { | ||
const valid = def.validateSchema(schema[keyword]) | ||
if (!valid) { | ||
const msg = "keyword value is invalid: " + self.errorsText(def.validateSchema.errors) | ||
if (opts.validateSchema === "log") self.logger.error(msg) | ||
else throw new Error(msg) | ||
} | ||
} | ||
} | ||
|
||
const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/ | ||
const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/ | ||
export function getData( | ||
$data: string, | ||
{dataLevel, dataNames, dataPathArr}: SchemaCxt | ||
): Code | number { | ||
let jsonPointer | ||
let data: Code | ||
if ($data === "") return N.rootData | ||
if ($data[0] === "/") { | ||
if (!JSON_POINTER.test($data)) throw new Error(`Invalid JSON-pointer: ${$data}`) | ||
jsonPointer = $data | ||
data = N.rootData | ||
} else { | ||
const matches = RELATIVE_JSON_POINTER.exec($data) | ||
if (!matches) throw new Error(`Invalid JSON-pointer: ${$data}`) | ||
const up: number = +matches[1] | ||
jsonPointer = matches[2] | ||
if (jsonPointer === "#") { | ||
if (up >= dataLevel) throw new Error(errorMsg("property/index", up)) | ||
return dataPathArr[dataLevel - up] | ||
} | ||
if (up > dataLevel) throw new Error(errorMsg("data", up)) | ||
data = dataNames[dataLevel - up] | ||
if (!jsonPointer) return data | ||
} | ||
|
||
let expr = data | ||
const segments = jsonPointer.split("/") | ||
for (const segment of segments) { | ||
if (segment) { | ||
data = _`${data}${getProperty(unescapeJsonPointer(segment))}` | ||
expr = _`${expr} && ${data}` | ||
} | ||
} | ||
return expr | ||
|
||
function errorMsg(pointerType: string, up: number): string { | ||
return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}` | ||
} | ||
} | ||
import {KeywordCxt} from "./validate" | ||
export default KeywordCxt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.