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

View File

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