diff --git a/performance/load-test.js b/performance/load-test.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/classes/field.js b/src/classes/field.js index 75092b2e..1bdb2f2b 100644 --- a/src/classes/field.js +++ b/src/classes/field.js @@ -63,6 +63,10 @@ const Field = class { return this.data.allowReferencing; } + get valueConstraint() { + return this.data.valueConstraint; + } + get standard() { return this.data.standard; } @@ -168,8 +172,7 @@ const Field = class { } if (!isEnum) { // Is this a URL template? - // This processes most strings... so could be a bit intensive - if (PropertyHelper.isUrlTemplate(data)) { + if (this.valueConstraint === 'UriTemplate' && PropertyHelper.isUrlTemplate(data)) { returnType = 'https://schema.org/Text'; } else if (DataModelHelper.getProperties(this.version).has(data)) { returnType = 'https://schema.org/Property'; diff --git a/src/errors/validation-error-type.js b/src/errors/validation-error-type.js index 44b20b40..6f33243c 100644 --- a/src/errors/validation-error-type.js +++ b/src/errors/validation-error-type.js @@ -40,7 +40,8 @@ const ValidationErrorType = { TYPE_LIMITS_USE: 'type_limits_use', WRONG_BASE_TYPE: 'wrong_base_type', FIELD_NOT_ALLOWED: 'field_not_allowed', - BELOW_MIN_VALUE_INCLUSIVE: 'BELOW_MIN_VALUE_INCLUSIVE', + BELOW_MIN_VALUE_INCLUSIVE: 'below_min_value_inclusive', + VALUE_OUTWITH_CONSTRAINT: 'value_outwith_constraint', }; module.exports = Object.freeze(ValidationErrorType); diff --git a/src/helpers/property.js b/src/helpers/property.js index 3bb2b755..60bd9a21 100644 --- a/src/helpers/property.js +++ b/src/helpers/property.js @@ -198,6 +198,10 @@ const PropertyHelper = class { return false; } + static isValidUUID(data) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(data); + } + static clearCache() { this.enumCache = {}; this.propertyCache = {}; diff --git a/src/rules/core/valueconstraint-rule-spec.js b/src/rules/core/valueconstraint-rule-spec.js new file mode 100644 index 00000000..14eed19d --- /dev/null +++ b/src/rules/core/valueconstraint-rule-spec.js @@ -0,0 +1,119 @@ +const ValueConstraintRule = require('./valueconstraint-rule'); +const Model = require('../../classes/model'); +const ModelNode = require('../../classes/model-node'); +const ValidationErrorType = require('../../errors/validation-error-type'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); + +describe('ValueConstraintRule', () => { + const rule = new ValueConstraintRule(); + + const model = new Model({ + type: 'Schedule', + fields: { + uuid: { + fieldName: 'uuid', + valueConstraint: 'UUID', + }, + uritemplate: { + fieldName: 'uritemplate', + valueConstraint: 'UriTemplate', + }, + }, + }, 'latest'); + model.hasSpecification = true; + + it('should target any field', () => { + const isTargeted = rule.isFieldTargeted(model, 'repeatCount'); + expect(isTargeted).toBe(true); + }); + + it('should return no errors for a value that is a valid UUID', async () => { + const values = [ + '123e4567-e89b-12d3-a456-426614174000', + '00000000-0000-0000-0000-000000000000', + ]; + + for (const value of values) { + const data = { + uuid: value, + }; + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + } + }); + + it('should return an error when the value is not a valid UUID', async () => { + const values = [ + '123E4567-E89B-12D3-A456-426614174000', + '000000000000000000000000000000000000', + '123e4567-e89b-12d3-a456-4266141740000', + '123e4567-e89b-12d3-a456-42661417400', + ]; + + for (const value of values) { + const data = { + uuid: value, + }; + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.VALUE_OUTWITH_CONSTRAINT); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + } + }); + + it('should return no errors for a value that is a valid URI Template', async () => { + const values = [ + 'https://api.example.org/session-series/123/{startDate}', + ]; + + for (const value of values) { + const data = { + uritemplate: value, + }; + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + expect(errors.length).toBe(0); + } + }); + + it('should return an error when the value is not a valid URI Template', async () => { + const values = [ + 'https://api.example.org/session-series/123/', + ]; + + for (const value of values) { + const data = { + uritemplate: value, + }; + const nodeToTest = new ModelNode( + '$', + data, + null, + model, + ); + const errors = await rule.validate(nodeToTest); + + expect(errors.length).toBe(1); + expect(errors[0].type).toBe(ValidationErrorType.VALUE_OUTWITH_CONSTRAINT); + expect(errors[0].severity).toBe(ValidationErrorSeverity.FAILURE); + } + }); +}); diff --git a/src/rules/core/valueconstraint-rule.js b/src/rules/core/valueconstraint-rule.js new file mode 100644 index 00000000..f1521b9f --- /dev/null +++ b/src/rules/core/valueconstraint-rule.js @@ -0,0 +1,65 @@ +const Rule = require('../rule'); +const PropertyHelper = require('../../helpers/property'); +const ValidationErrorType = require('../../errors/validation-error-type'); +const ValidationErrorCategory = require('../../errors/validation-error-category'); +const ValidationErrorSeverity = require('../../errors/validation-error-severity'); + +module.exports = class ValueConstraintRule extends Rule { + constructor(options) { + super(options); + this.targetFields = '*'; + this.meta = { + name: 'ValueConstraintRule', + description: 'Validates that all properties meet the associated valueConstraint parameter.', + tests: { + valueConstraint: { + description: 'Raises a failure if the value does not match the associated constraint', + message: 'The value of this property did not match the expected "{{valueConstraint}}" format.', + sampleValues: { + valueConstraint: 'UriTemplate', + }, + category: ValidationErrorCategory.DATA_QUALITY, + severity: ValidationErrorSeverity.FAILURE, + type: ValidationErrorType.VALUE_OUTWITH_CONSTRAINT, + }, + }, + }; + } + + validateField(node, field) { + // Don't do this check for models that we don't actually have a spec for + if (!node.model.hasSpecification) { + return []; + } + if (!node.model.hasField(field)) { + return []; + } + + const errors = []; + + // Get the field object + const fieldObj = node.model.getField(field); + const fieldValue = node.getMappedValue(field); + + if (typeof fieldObj.valueConstraint !== 'undefined' + && ((fieldObj.valueConstraint === 'UriTemplate' && !PropertyHelper.isUrlTemplate(fieldValue)) + || (fieldObj.valueConstraint === 'UUID' && !PropertyHelper.isValidUUID(fieldValue)))) { + errors.push( + this.createError( + 'valueConstraint', + { + fieldValue, + path: node.getPath(field), + }, + { + valueConstraint: fieldObj.valueConstraint, + }, + ), + ); + } else { + return []; + } + + return errors; + } +};