Json schema improvements (#1698)

* Simplify schema multi-push/pop

* Reverse order of schema path

* Reverse order of value path

* Simplify schema path structure

* Rename for better clarity
This commit is contained in:
toasted-nutbread 2021-05-22 17:56:44 -04:00 committed by GitHub
parent d16739a83a
commit d7cf019b4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 92 additions and 91 deletions

View File

@ -25,8 +25,8 @@ class JsonSchema {
this._startSchema = schema;
this._rootSchema = typeof rootSchema !== 'undefined' ? rootSchema : schema;
this._regexCache = null;
this._valuePath = [];
this._schemaPath = [];
this._valueStack = [];
this._schemaStack = [];
this._schemaPush(null, null);
this._valuePush(null, null);
@ -58,8 +58,8 @@ class JsonSchema {
}
validate(value) {
this._schemaPush(null, this._startSchema);
this._valuePush(null, value);
this._schemaPush(this._startSchema, null);
this._valuePush(value, null);
try {
this._validate(value);
} finally {
@ -69,24 +69,24 @@ class JsonSchema {
}
getValidValueOrDefault(value) {
return this._getValidValueOrDefault(null, value, [{path: null, schema: this._startSchema}]);
return this._getValidValueOrDefault(null, value, {schema: this._startSchema, path: null});
}
getObjectPropertySchema(property) {
this._schemaPush(null, this._startSchema);
this._schemaPush(this._startSchema, null);
try {
const schemaPath = this._getObjectPropertySchemaPath(property);
return schemaPath !== null ? new JsonSchema(schemaPath[schemaPath.length - 1].schema, this._rootSchema) : null;
const schemaInfo = this._getObjectPropertySchemaPath(property);
return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null;
} finally {
this._schemaPop();
}
}
getArrayItemSchema(index) {
this._schemaPush(null, this._startSchema);
this._schemaPush(this._startSchema, null);
try {
const schemaPath = this._getArrayItemSchemaPath(index);
return schemaPath !== null ? new JsonSchema(schemaPath[schemaPath.length - 1].schema, this._rootSchema) : null;
const schemaInfo = this._getArrayItemSchemaPath(index);
return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null;
} finally {
this._schemaPop();
}
@ -99,44 +99,44 @@ class JsonSchema {
// Stack
_valuePush(path, value) {
this._valuePath.push({path, value});
_valuePush(value, path) {
this._valueStack.push({value, path});
}
_valuePop() {
this._valuePath.pop();
this._valueStack.pop();
}
_schemaPush(path, schema) {
this._schemaPath.push({path, schema});
_schemaPush(schema, path) {
this._schemaStack.push({schema, path});
this._schema = schema;
}
_schemaPop() {
this._schemaPath.pop();
this._schema = this._schemaPath[this._schemaPath.length - 1].schema;
this._schemaStack.pop();
this._schema = this._schemaStack[this._schemaStack.length - 1].schema;
}
// Private
_createError(message) {
const valuePath = [];
for (let i = 1, ii = this._valuePath.length; i < ii; ++i) {
const {path, value} = this._valuePath[i];
valuePath.push({path, value});
const valueStack = [];
for (let i = 1, ii = this._valueStack.length; i < ii; ++i) {
const {value, path} = this._valueStack[i];
valueStack.push({value, path});
}
const schemaPath = [];
for (let i = 1, ii = this._schemaPath.length; i < ii; ++i) {
const {path, schema} = this._schemaPath[i];
schemaPath.push({path, schema});
const schemaStack = [];
for (let i = 1, ii = this._schemaStack.length; i < ii; ++i) {
const {schema, path} = this._schemaStack[i];
schemaStack.push({schema, path});
}
const error = new Error(message);
error.value = valuePath[valuePath.length - 1].value;
error.value = valueStack[valueStack.length - 1].value;
error.schema = this._schema;
error.valuePath = valuePath;
error.schemaPath = schemaPath;
error.valueStack = valueStack;
error.schemaStack = schemaStack;
return error;
}
@ -167,10 +167,7 @@ class JsonSchema {
if (this._isObject(properties)) {
const propertySchema = properties[property];
if (this._isObject(propertySchema)) {
return [
{path: 'properties', schema: properties},
{path: property, schema: propertySchema}
];
return {schema: propertySchema, path: ['properties', property]};
}
}
@ -178,26 +175,23 @@ class JsonSchema {
if (additionalProperties === false) {
return null;
} else if (this._isObject(additionalProperties)) {
return [{path: 'additionalProperties', schema: additionalProperties}];
return {schema: additionalProperties, path: 'additionalProperties'};
} else {
const result = this._getUnconstrainedSchema();
return [{path: null, schema: result}];
return {schema: result, path: null};
}
}
_getArrayItemSchemaPath(index) {
const {items} = this._schema;
if (this._isObject(items)) {
return [{path: 'items', schema: items}];
return {schema: items, path: 'items'};
}
if (Array.isArray(items)) {
if (index >= 0 && index < items.length) {
const propertySchema = items[index];
if (this._isObject(propertySchema)) {
return [
{path: 'items', schema: items},
{path: index, schema: propertySchema}
];
return {schema: propertySchema, path: ['items', index]};
}
}
}
@ -206,10 +200,10 @@ class JsonSchema {
if (additionalItems === false) {
return null;
} else if (this._isObject(additionalItems)) {
return [{path: 'additionalItems', schema: additionalItems}];
return {schema: additionalItems, path: 'additionalItems'};
} else {
const result = this._getUnconstrainedSchema();
return [{path: null, schema: result}];
return {schema: result, path: null};
}
}
@ -298,7 +292,7 @@ class JsonSchema {
if (!this._isObject(ifSchema)) { return; }
let okay = true;
this._schemaPush('if', ifSchema);
this._schemaPush(ifSchema, 'if');
try {
this._validate(value);
} catch (e) {
@ -310,7 +304,7 @@ class JsonSchema {
const nextSchema = okay ? this._schema.then : this._schema.else;
if (this._isObject(nextSchema)) { return; }
this._schemaPush(okay ? 'then' : 'else', nextSchema);
this._schemaPush(nextSchema, okay ? 'then' : 'else');
try {
this._validate(value);
} finally {
@ -322,13 +316,13 @@ class JsonSchema {
const subSchemas = this._schema.allOf;
if (!Array.isArray(subSchemas)) { return; }
this._schemaPush('allOf', subSchemas);
this._schemaPush(subSchemas, 'allOf');
try {
for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
const subSchema = subSchemas[i];
if (!this._isObject(subSchema)) { continue; }
this._schemaPush(i, subSchema);
this._schemaPush(subSchema, i);
try {
this._validate(value);
} finally {
@ -344,13 +338,13 @@ class JsonSchema {
const subSchemas = this._schema.anyOf;
if (!Array.isArray(subSchemas)) { return; }
this._schemaPush('anyOf', subSchemas);
this._schemaPush(subSchemas, 'anyOf');
try {
for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
const subSchema = subSchemas[i];
if (!this._isObject(subSchema)) { continue; }
this._schemaPush(i, subSchema);
this._schemaPush(subSchema, i);
try {
this._validate(value);
return;
@ -371,14 +365,14 @@ class JsonSchema {
const subSchemas = this._schema.oneOf;
if (!Array.isArray(subSchemas)) { return; }
this._schemaPush('oneOf', subSchemas);
this._schemaPush(subSchemas, 'oneOf');
try {
let count = 0;
for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
const subSchema = subSchemas[i];
if (!this._isObject(subSchema)) { continue; }
this._schemaPush(i, subSchema);
this._schemaPush(subSchema, i);
try {
this._validate(value);
++count;
@ -401,13 +395,13 @@ class JsonSchema {
const subSchemas = this._schema.not;
if (!Array.isArray(subSchemas)) { return; }
this._schemaPush('not', subSchemas);
this._schemaPush(subSchemas, 'not');
try {
for (let i = 0, ii = subSchemas.length; i < ii; ++i) {
const subSchema = subSchemas[i];
if (!this._isObject(subSchema)) { continue; }
this._schemaPush(i, subSchema);
this._schemaPush(subSchema, i);
try {
this._validate(value);
} catch (e) {
@ -527,20 +521,20 @@ class JsonSchema {
this._validateArrayContains(value);
for (let i = 0; i < length; ++i) {
const schemaPath = this._getArrayItemSchemaPath(i);
if (schemaPath === null) {
const schemaInfo = this._getArrayItemSchemaPath(i);
if (schemaInfo === null) {
throw this._createError(`No schema found for array[${i}]`);
}
const propertyValue = value[i];
for (const {path, schema} of schemaPath) { this._schemaPush(path, schema); }
this._valuePush(i, propertyValue);
this._schemaPush(schemaInfo.schema, schemaInfo.path);
this._valuePush(propertyValue, i);
try {
this._validate(propertyValue);
} finally {
this._valuePop();
for (let j = 0, jj = schemaPath.length; j < jj; ++j) { this._schemaPop(); }
this._schemaPop();
}
}
}
@ -549,11 +543,11 @@ class JsonSchema {
const containsSchema = this._schema.contains;
if (!this._isObject(containsSchema)) { return; }
this._schemaPush('contains', containsSchema);
this._schemaPush(containsSchema, 'contains');
try {
for (let i = 0, ii = value.length; i < ii; ++i) {
const propertyValue = value[i];
this._valuePush(i, propertyValue);
this._valuePush(propertyValue, i);
try {
this._validate(propertyValue);
return;
@ -592,20 +586,20 @@ class JsonSchema {
}
for (const property of properties) {
const schemaPath = this._getObjectPropertySchemaPath(property);
if (schemaPath === null) {
const schemaInfo = this._getObjectPropertySchemaPath(property);
if (schemaInfo === null) {
throw this._createError(`No schema found for ${property}`);
}
const propertyValue = value[property];
for (const {path, schema} of schemaPath) { this._schemaPush(path, schema); }
this._valuePush(property, propertyValue);
this._schemaPush(schemaInfo.schema, schemaInfo.path);
this._valuePush(propertyValue, property);
try {
this._validate(propertyValue);
} finally {
this._valuePop();
for (let j = 0, jj = schemaPath.length; j < jj; ++j) { this._schemaPop(); }
this._schemaPop();
}
}
}
@ -643,14 +637,14 @@ class JsonSchema {
);
}
_getValidValueOrDefault(path, value, schemaPath) {
this._valuePush(path, value);
for (const {path: path2, schema} of schemaPath) { this._schemaPush(path2, schema); }
_getValidValueOrDefault(path, value, schemaInfo) {
this._schemaPush(schemaInfo.schema, schemaInfo.path);
this._valuePush(value, path);
try {
return this._getValidValueOrDefaultInner(value);
} finally {
for (let i = 0, ii = schemaPath.length; i < ii; ++i) { this._schemaPop(); }
this._valuePop();
this._schemaPop();
}
}
@ -688,19 +682,19 @@ class JsonSchema {
if (Array.isArray(required)) {
for (const property of required) {
properties.delete(property);
const schemaPath = this._getObjectPropertySchemaPath(property);
if (schemaPath === null) { continue; }
const schemaInfo = this._getObjectPropertySchemaPath(property);
if (schemaInfo === null) { continue; }
const propertyValue = Object.prototype.hasOwnProperty.call(value, property) ? value[property] : void 0;
value[property] = this._getValidValueOrDefault(property, propertyValue, schemaPath);
value[property] = this._getValidValueOrDefault(property, propertyValue, schemaInfo);
}
}
for (const property of properties) {
const schemaPath = this._getObjectPropertySchemaPath(property);
if (schemaPath === null) {
const schemaInfo = this._getObjectPropertySchemaPath(property);
if (schemaInfo === null) {
Reflect.deleteProperty(value, property);
} else {
value[property] = this._getValidValueOrDefault(property, value[property], schemaPath);
value[property] = this._getValidValueOrDefault(property, value[property], schemaInfo);
}
}
@ -709,18 +703,18 @@ class JsonSchema {
_populateArrayDefaults(value) {
for (let i = 0, ii = value.length; i < ii; ++i) {
const schemaPath = this._getArrayItemSchemaPath(i);
if (schemaPath === null) { continue; }
const schemaInfo = this._getArrayItemSchemaPath(i);
if (schemaInfo === null) { continue; }
const propertyValue = value[i];
value[i] = this._getValidValueOrDefault(i, propertyValue, schemaPath);
value[i] = this._getValidValueOrDefault(i, propertyValue, schemaInfo);
}
const {minItems, maxItems} = this._schema;
if (typeof minItems === 'number' && value.length < minItems) {
for (let i = value.length; i < minItems; ++i) {
const schemaPath = this._getArrayItemSchemaPath(i);
if (schemaPath === null) { break; }
const item = this._getValidValueOrDefault(i, void 0, schemaPath);
const schemaInfo = this._getArrayItemSchemaPath(i);
if (schemaInfo === null) { break; }
const item = this._getValidValueOrDefault(i, void 0, schemaInfo);
value.push(item);
}
}

View File

@ -253,8 +253,8 @@ class DictionaryImporter {
}
_formatSchemaError(e, fileName) {
const valuePathString = this._getSchemaErrorPathString(e.valuePath, 'dictionary');
const schemaPathString = this._getSchemaErrorPathString(e.schemaPath, 'schema');
const valuePathString = this._getSchemaErrorPathString(e.valueStack, 'dictionary');
const schemaPathString = this._getSchemaErrorPathString(e.schemaStack, 'schema');
const e2 = new Error(`Dictionary has invalid data in '${fileName}' for value '${valuePathString}', validated against '${schemaPathString}': ${e.message}`);
e2.data = e;
@ -265,16 +265,23 @@ class DictionaryImporter {
_getSchemaErrorPathString(infoList, base='') {
let result = base;
for (const {path} of infoList) {
switch (typeof path) {
case 'string':
if (result.length > 0) {
result += '.';
const pathArray = Array.isArray(path) ? path : [path];
for (const pathPart of pathArray) {
if (pathPart === null) {
result = base;
} else {
switch (typeof pathPart) {
case 'string':
if (result.length > 0) {
result += '.';
}
result += pathPart;
break;
case 'number':
result += `[${pathPart}]`;
break;
}
result += path;
break;
case 'number':
result += `[${path}]`;
break;
}
}
}
return result;