You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
510 lines
19 KiB
510 lines
19 KiB
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getData = exports.KeywordCxt = exports.validateFunctionCode = void 0;
|
|
const boolSchema_1 = require("./boolSchema");
|
|
const dataType_1 = require("./dataType");
|
|
const applicability_1 = require("./applicability");
|
|
const dataType_2 = require("./dataType");
|
|
const defaults_1 = require("./defaults");
|
|
const keyword_1 = require("./keyword");
|
|
const subschema_1 = require("./subschema");
|
|
const codegen_1 = require("../codegen");
|
|
const names_1 = require("../names");
|
|
const resolve_1 = require("../resolve");
|
|
const util_1 = require("../util");
|
|
const errors_1 = require("../errors");
|
|
// schema compilation - generates validation function, subschemaCode (below) is used for subschemas
|
|
function validateFunctionCode(it) {
|
|
if (isSchemaObj(it)) {
|
|
checkKeywords(it);
|
|
if (schemaCxtHasRules(it)) {
|
|
topSchemaObjCode(it);
|
|
return;
|
|
}
|
|
}
|
|
validateFunction(it, () => boolSchema_1.topBoolOrEmptySchema(it));
|
|
}
|
|
exports.validateFunctionCode = validateFunctionCode;
|
|
function validateFunction({ gen, validateName, schema, schemaEnv, opts }, body) {
|
|
if (opts.code.es5) {
|
|
gen.func(validateName, codegen_1._ `${names_1.default.data}, ${names_1.default.valCxt}`, schemaEnv.$async, () => {
|
|
gen.code(codegen_1._ `"use strict"; ${funcSourceUrl(schema, opts)}`);
|
|
destructureValCxtES5(gen, opts);
|
|
gen.code(body);
|
|
});
|
|
}
|
|
else {
|
|
gen.func(validateName, codegen_1._ `${names_1.default.data}, ${destructureValCxt(opts)}`, schemaEnv.$async, () => gen.code(funcSourceUrl(schema, opts)).code(body));
|
|
}
|
|
}
|
|
function destructureValCxt(opts) {
|
|
return codegen_1._ `{${names_1.default.instancePath}="", ${names_1.default.parentData}, ${names_1.default.parentDataProperty}, ${names_1.default.rootData}=${names_1.default.data}${opts.dynamicRef ? codegen_1._ `, ${names_1.default.dynamicAnchors}={}` : codegen_1.nil}}={}`;
|
|
}
|
|
function destructureValCxtES5(gen, opts) {
|
|
gen.if(names_1.default.valCxt, () => {
|
|
gen.var(names_1.default.instancePath, codegen_1._ `${names_1.default.valCxt}.${names_1.default.instancePath}`);
|
|
gen.var(names_1.default.parentData, codegen_1._ `${names_1.default.valCxt}.${names_1.default.parentData}`);
|
|
gen.var(names_1.default.parentDataProperty, codegen_1._ `${names_1.default.valCxt}.${names_1.default.parentDataProperty}`);
|
|
gen.var(names_1.default.rootData, codegen_1._ `${names_1.default.valCxt}.${names_1.default.rootData}`);
|
|
if (opts.dynamicRef)
|
|
gen.var(names_1.default.dynamicAnchors, codegen_1._ `${names_1.default.valCxt}.${names_1.default.dynamicAnchors}`);
|
|
}, () => {
|
|
gen.var(names_1.default.instancePath, codegen_1._ `""`);
|
|
gen.var(names_1.default.parentData, codegen_1._ `undefined`);
|
|
gen.var(names_1.default.parentDataProperty, codegen_1._ `undefined`);
|
|
gen.var(names_1.default.rootData, names_1.default.data);
|
|
if (opts.dynamicRef)
|
|
gen.var(names_1.default.dynamicAnchors, codegen_1._ `{}`);
|
|
});
|
|
}
|
|
function topSchemaObjCode(it) {
|
|
const { schema, opts, gen } = it;
|
|
validateFunction(it, () => {
|
|
if (opts.$comment && schema.$comment)
|
|
commentKeyword(it);
|
|
checkNoDefault(it);
|
|
gen.let(names_1.default.vErrors, null);
|
|
gen.let(names_1.default.errors, 0);
|
|
if (opts.unevaluated)
|
|
resetEvaluated(it);
|
|
typeAndKeywords(it);
|
|
returnResults(it);
|
|
});
|
|
return;
|
|
}
|
|
function resetEvaluated(it) {
|
|
// TODO maybe some hook to execute it in the end to check whether props/items are Name, as in assignEvaluated
|
|
const { gen, validateName } = it;
|
|
it.evaluated = gen.const("evaluated", codegen_1._ `${validateName}.evaluated`);
|
|
gen.if(codegen_1._ `${it.evaluated}.dynamicProps`, () => gen.assign(codegen_1._ `${it.evaluated}.props`, codegen_1._ `undefined`));
|
|
gen.if(codegen_1._ `${it.evaluated}.dynamicItems`, () => gen.assign(codegen_1._ `${it.evaluated}.items`, codegen_1._ `undefined`));
|
|
}
|
|
function funcSourceUrl(schema, opts) {
|
|
const schId = typeof schema == "object" && schema[opts.schemaId];
|
|
return schId && (opts.code.source || opts.code.process) ? codegen_1._ `/*# sourceURL=${schId} */` : codegen_1.nil;
|
|
}
|
|
// schema compilation - this function is used recursively to generate code for sub-schemas
|
|
function subschemaCode(it, valid) {
|
|
if (isSchemaObj(it)) {
|
|
checkKeywords(it);
|
|
if (schemaCxtHasRules(it)) {
|
|
subSchemaObjCode(it, valid);
|
|
return;
|
|
}
|
|
}
|
|
boolSchema_1.boolOrEmptySchema(it, valid);
|
|
}
|
|
function schemaCxtHasRules({ schema, self }) {
|
|
if (typeof schema == "boolean")
|
|
return !schema;
|
|
for (const key in schema)
|
|
if (self.RULES.all[key])
|
|
return true;
|
|
return false;
|
|
}
|
|
function isSchemaObj(it) {
|
|
return typeof it.schema != "boolean";
|
|
}
|
|
function subSchemaObjCode(it, valid) {
|
|
const { schema, gen, opts } = it;
|
|
if (opts.$comment && schema.$comment)
|
|
commentKeyword(it);
|
|
updateContext(it);
|
|
checkAsyncSchema(it);
|
|
const errsCount = gen.const("_errs", names_1.default.errors);
|
|
typeAndKeywords(it, errsCount);
|
|
// TODO var
|
|
gen.var(valid, codegen_1._ `${errsCount} === ${names_1.default.errors}`);
|
|
}
|
|
function checkKeywords(it) {
|
|
util_1.checkUnknownRules(it);
|
|
checkRefsAndKeywords(it);
|
|
}
|
|
function typeAndKeywords(it, errsCount) {
|
|
if (it.opts.jtd)
|
|
return schemaKeywords(it, [], false, errsCount);
|
|
const types = dataType_1.getSchemaTypes(it.schema);
|
|
const checkedTypes = dataType_1.coerceAndCheckDataType(it, types);
|
|
schemaKeywords(it, types, !checkedTypes, errsCount);
|
|
}
|
|
function checkRefsAndKeywords(it) {
|
|
const { schema, errSchemaPath, opts, self } = it;
|
|
if (schema.$ref && opts.ignoreKeywordsWithRef && util_1.schemaHasRulesButRef(schema, self.RULES)) {
|
|
self.logger.warn(`$ref: keywords ignored in schema at path "${errSchemaPath}"`);
|
|
}
|
|
}
|
|
function checkNoDefault(it) {
|
|
const { schema, opts } = it;
|
|
if (schema.default !== undefined && opts.useDefaults && opts.strictSchema) {
|
|
util_1.checkStrictMode(it, "default is ignored in the schema root");
|
|
}
|
|
}
|
|
function updateContext(it) {
|
|
const schId = it.schema[it.opts.schemaId];
|
|
if (schId)
|
|
it.baseId = resolve_1.resolveUrl(it.baseId, schId);
|
|
}
|
|
function checkAsyncSchema(it) {
|
|
if (it.schema.$async && !it.schemaEnv.$async)
|
|
throw new Error("async schema in sync schema");
|
|
}
|
|
function commentKeyword({ gen, schemaEnv, schema, errSchemaPath, opts }) {
|
|
const msg = schema.$comment;
|
|
if (opts.$comment === true) {
|
|
gen.code(codegen_1._ `${names_1.default.self}.logger.log(${msg})`);
|
|
}
|
|
else if (typeof opts.$comment == "function") {
|
|
const schemaPath = codegen_1.str `${errSchemaPath}/$comment`;
|
|
const rootName = gen.scopeValue("root", { ref: schemaEnv.root });
|
|
gen.code(codegen_1._ `${names_1.default.self}.opts.$comment(${msg}, ${schemaPath}, ${rootName}.schema)`);
|
|
}
|
|
}
|
|
function returnResults(it) {
|
|
const { gen, schemaEnv, validateName, ValidationError, opts } = it;
|
|
if (schemaEnv.$async) {
|
|
// TODO assign unevaluated
|
|
gen.if(codegen_1._ `${names_1.default.errors} === 0`, () => gen.return(names_1.default.data), () => gen.throw(codegen_1._ `new ${ValidationError}(${names_1.default.vErrors})`));
|
|
}
|
|
else {
|
|
gen.assign(codegen_1._ `${validateName}.errors`, names_1.default.vErrors);
|
|
if (opts.unevaluated)
|
|
assignEvaluated(it);
|
|
gen.return(codegen_1._ `${names_1.default.errors} === 0`);
|
|
}
|
|
}
|
|
function assignEvaluated({ gen, evaluated, props, items }) {
|
|
if (props instanceof codegen_1.Name)
|
|
gen.assign(codegen_1._ `${evaluated}.props`, props);
|
|
if (items instanceof codegen_1.Name)
|
|
gen.assign(codegen_1._ `${evaluated}.items`, items);
|
|
}
|
|
function schemaKeywords(it, types, typeErrors, errsCount) {
|
|
const { gen, schema, data, allErrors, opts, self } = it;
|
|
const { RULES } = self;
|
|
if (schema.$ref && (opts.ignoreKeywordsWithRef || !util_1.schemaHasRulesButRef(schema, RULES))) {
|
|
gen.block(() => keywordCode(it, "$ref", RULES.all.$ref.definition)); // TODO typecast
|
|
return;
|
|
}
|
|
if (!opts.jtd)
|
|
checkStrictTypes(it, types);
|
|
gen.block(() => {
|
|
for (const group of RULES.rules)
|
|
groupKeywords(group);
|
|
groupKeywords(RULES.post);
|
|
});
|
|
function groupKeywords(group) {
|
|
if (!applicability_1.shouldUseGroup(schema, group))
|
|
return;
|
|
if (group.type) {
|
|
gen.if(dataType_2.checkDataType(group.type, data, opts.strictNumbers));
|
|
iterateKeywords(it, group);
|
|
if (types.length === 1 && types[0] === group.type && typeErrors) {
|
|
gen.else();
|
|
dataType_2.reportTypeError(it);
|
|
}
|
|
gen.endIf();
|
|
}
|
|
else {
|
|
iterateKeywords(it, group);
|
|
}
|
|
// TODO make it "ok" call?
|
|
if (!allErrors)
|
|
gen.if(codegen_1._ `${names_1.default.errors} === ${errsCount || 0}`);
|
|
}
|
|
}
|
|
function iterateKeywords(it, group) {
|
|
const { gen, schema, opts: { useDefaults }, } = it;
|
|
if (useDefaults)
|
|
defaults_1.assignDefaults(it, group.type);
|
|
gen.block(() => {
|
|
for (const rule of group.rules) {
|
|
if (applicability_1.shouldUseRule(schema, rule)) {
|
|
keywordCode(it, rule.keyword, rule.definition, group.type);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
function checkStrictTypes(it, types) {
|
|
if (it.schemaEnv.meta || !it.opts.strictTypes)
|
|
return;
|
|
checkContextTypes(it, types);
|
|
if (!it.opts.allowUnionTypes)
|
|
checkMultipleTypes(it, types);
|
|
checkKeywordTypes(it, it.dataTypes);
|
|
}
|
|
function checkContextTypes(it, types) {
|
|
if (!types.length)
|
|
return;
|
|
if (!it.dataTypes.length) {
|
|
it.dataTypes = types;
|
|
return;
|
|
}
|
|
types.forEach((t) => {
|
|
if (!includesType(it.dataTypes, t)) {
|
|
strictTypesError(it, `type "${t}" not allowed by context "${it.dataTypes.join(",")}"`);
|
|
}
|
|
});
|
|
it.dataTypes = it.dataTypes.filter((t) => includesType(types, t));
|
|
}
|
|
function checkMultipleTypes(it, ts) {
|
|
if (ts.length > 1 && !(ts.length === 2 && ts.includes("null"))) {
|
|
strictTypesError(it, "use allowUnionTypes to allow union type keyword");
|
|
}
|
|
}
|
|
function checkKeywordTypes(it, ts) {
|
|
const rules = it.self.RULES.all;
|
|
for (const keyword in rules) {
|
|
const rule = rules[keyword];
|
|
if (typeof rule == "object" && applicability_1.shouldUseRule(it.schema, rule)) {
|
|
const { type } = rule.definition;
|
|
if (type.length && !type.some((t) => hasApplicableType(ts, t))) {
|
|
strictTypesError(it, `missing type "${type.join(",")}" for keyword "${keyword}"`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function hasApplicableType(schTs, kwdT) {
|
|
return schTs.includes(kwdT) || (kwdT === "number" && schTs.includes("integer"));
|
|
}
|
|
function includesType(ts, t) {
|
|
return ts.includes(t) || (t === "integer" && ts.includes("number"));
|
|
}
|
|
function strictTypesError(it, msg) {
|
|
const schemaPath = it.schemaEnv.baseId + it.errSchemaPath;
|
|
msg += ` at "${schemaPath}" (strictTypes)`;
|
|
util_1.checkStrictMode(it, msg, it.opts.strictTypes);
|
|
}
|
|
class KeywordCxt {
|
|
constructor(it, def, keyword) {
|
|
keyword_1.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 = util_1.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 (!keyword_1.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", names_1.default.errors);
|
|
}
|
|
}
|
|
result(condition, successAction, failAction) {
|
|
this.failResult(codegen_1.not(condition), successAction, failAction);
|
|
}
|
|
failResult(condition, successAction, failAction) {
|
|
this.gen.if(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, failAction) {
|
|
this.failResult(codegen_1.not(condition), undefined, failAction);
|
|
}
|
|
fail(condition) {
|
|
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) {
|
|
if (!this.$data)
|
|
return this.fail(condition);
|
|
const { schemaCode } = this;
|
|
this.fail(codegen_1._ `${schemaCode} !== undefined && (${codegen_1.or(this.invalid$data(), condition)})`);
|
|
}
|
|
error(append, errorParams, errorPaths) {
|
|
if (errorParams) {
|
|
this.setParams(errorParams);
|
|
this._error(append, errorPaths);
|
|
this.setParams({});
|
|
return;
|
|
}
|
|
this._error(append, errorPaths);
|
|
}
|
|
_error(append, errorPaths) {
|
|
;
|
|
(append ? errors_1.reportExtraError : errors_1.reportError)(this, this.def.error, errorPaths);
|
|
}
|
|
$dataError() {
|
|
errors_1.reportError(this, this.def.$dataError || errors_1.keyword$DataError);
|
|
}
|
|
reset() {
|
|
if (this.errsCount === undefined)
|
|
throw new Error('add "trackErrors" to keyword definition');
|
|
errors_1.resetErrorsCount(this.gen, this.errsCount);
|
|
}
|
|
ok(cond) {
|
|
if (!this.allErrors)
|
|
this.gen.if(cond);
|
|
}
|
|
setParams(obj, assign) {
|
|
if (assign)
|
|
Object.assign(this.params, obj);
|
|
else
|
|
this.params = obj;
|
|
}
|
|
block$data(valid, codeBlock, $dataValid = codegen_1.nil) {
|
|
this.gen.block(() => {
|
|
this.check$data(valid, $dataValid);
|
|
codeBlock();
|
|
});
|
|
}
|
|
check$data(valid = codegen_1.nil, $dataValid = codegen_1.nil) {
|
|
if (!this.$data)
|
|
return;
|
|
const { gen, schemaCode, schemaType, def } = this;
|
|
gen.if(codegen_1.or(codegen_1._ `${schemaCode} === undefined`, $dataValid));
|
|
if (valid !== codegen_1.nil)
|
|
gen.assign(valid, true);
|
|
if (schemaType.length || def.validateSchema) {
|
|
gen.elseIf(this.invalid$data());
|
|
this.$dataError();
|
|
if (valid !== codegen_1.nil)
|
|
gen.assign(valid, false);
|
|
}
|
|
gen.else();
|
|
}
|
|
invalid$data() {
|
|
const { gen, schemaCode, schemaType, def, it } = this;
|
|
return codegen_1.or(wrong$DataType(), invalid$DataSchema());
|
|
function wrong$DataType() {
|
|
if (schemaType.length) {
|
|
/* istanbul ignore if */
|
|
if (!(schemaCode instanceof codegen_1.Name))
|
|
throw new Error("ajv implementation error");
|
|
const st = Array.isArray(schemaType) ? schemaType : [schemaType];
|
|
return codegen_1._ `${dataType_2.checkDataTypes(st, schemaCode, it.opts.strictNumbers, dataType_2.DataType.Wrong)}`;
|
|
}
|
|
return codegen_1.nil;
|
|
}
|
|
function invalid$DataSchema() {
|
|
if (def.validateSchema) {
|
|
const validateSchemaRef = gen.scopeValue("validate$data", { ref: def.validateSchema }); // TODO value.code for standalone
|
|
return codegen_1._ `!${validateSchemaRef}(${schemaCode})`;
|
|
}
|
|
return codegen_1.nil;
|
|
}
|
|
}
|
|
subschema(appl, valid) {
|
|
const subschema = subschema_1.getSubschema(this.it, appl);
|
|
subschema_1.extendSubschemaData(subschema, this.it, appl);
|
|
subschema_1.extendSubschemaMode(subschema, appl);
|
|
const nextContext = { ...this.it, ...subschema, items: undefined, props: undefined };
|
|
subschemaCode(nextContext, valid);
|
|
return nextContext;
|
|
}
|
|
mergeEvaluated(schemaCxt, toName) {
|
|
const { it, gen } = this;
|
|
if (!it.opts.unevaluated)
|
|
return;
|
|
if (it.props !== true && schemaCxt.props !== undefined) {
|
|
it.props = util_1.mergeEvaluated.props(gen, schemaCxt.props, it.props, toName);
|
|
}
|
|
if (it.items !== true && schemaCxt.items !== undefined) {
|
|
it.items = util_1.mergeEvaluated.items(gen, schemaCxt.items, it.items, toName);
|
|
}
|
|
}
|
|
mergeValidEvaluated(schemaCxt, valid) {
|
|
const { it, gen } = this;
|
|
if (it.opts.unevaluated && (it.props !== true || it.items !== true)) {
|
|
gen.if(valid, () => this.mergeEvaluated(schemaCxt, codegen_1.Name));
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
exports.KeywordCxt = KeywordCxt;
|
|
function keywordCode(it, keyword, def, ruleType) {
|
|
const cxt = new KeywordCxt(it, def, keyword);
|
|
if ("code" in def) {
|
|
def.code(cxt, ruleType);
|
|
}
|
|
else if (cxt.$data && def.validate) {
|
|
keyword_1.funcKeywordCode(cxt, def);
|
|
}
|
|
else if ("macro" in def) {
|
|
keyword_1.macroKeywordCode(cxt, def);
|
|
}
|
|
else if (def.compile || def.validate) {
|
|
keyword_1.funcKeywordCode(cxt, def);
|
|
}
|
|
}
|
|
const JSON_POINTER = /^\/(?:[^~]|~0|~1)*$/;
|
|
const RELATIVE_JSON_POINTER = /^([0-9]+)(#|\/(?:[^~]|~0|~1)*)?$/;
|
|
function getData($data, { dataLevel, dataNames, dataPathArr }) {
|
|
let jsonPointer;
|
|
let data;
|
|
if ($data === "")
|
|
return names_1.default.rootData;
|
|
if ($data[0] === "/") {
|
|
if (!JSON_POINTER.test($data))
|
|
throw new Error(`Invalid JSON-pointer: ${$data}`);
|
|
jsonPointer = $data;
|
|
data = names_1.default.rootData;
|
|
}
|
|
else {
|
|
const matches = RELATIVE_JSON_POINTER.exec($data);
|
|
if (!matches)
|
|
throw new Error(`Invalid JSON-pointer: ${$data}`);
|
|
const up = +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 = codegen_1._ `${data}${codegen_1.getProperty(util_1.unescapeJsonPointer(segment))}`;
|
|
expr = codegen_1._ `${expr} && ${data}`;
|
|
}
|
|
}
|
|
return expr;
|
|
function errorMsg(pointerType, up) {
|
|
return `Cannot access ${pointerType} ${up} levels up, current level is ${dataLevel}`;
|
|
}
|
|
}
|
|
exports.getData = getData;
|
|
//# sourceMappingURL=index.js.map
|