UNPKG

@accordproject/concerto-core

Version:

Core Implementation for the Concerto Modeling Language

555 lines (497 loc) • 20.5 kB
/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; const Util = require('@accordproject/concerto-util').NullUtil; const ModelUtil = require('../modelutil'); const ValidationException = require('./validationexception'); const Globalize = require('../globalize'); const dayjs = require('dayjs'); const utc = require('dayjs/plugin/utc'); dayjs.extend(utc); /** * <p> * Validates a Resource or Field against the models defined in the ModelManager. * This class is used with the Visitor pattern and visits the class declarations * (etc) for the model, checking that the data in a Resource / Field is consistent * with the model. * </p> * The parameters for the visit method must contain the following properties: * <ul> * <li> 'stack' - the TypedStack of objects being processed. It should * start as [Resource] or [Field]</li> * <li> 'rootResourceIdentifier' - the identifier of the resource being validated </li> * <li> 'modelManager' - the ModelManager instance to use for type checking</li> * </ul> * @private * @class * @memberof module:concerto-core */ class ObjectValidator { /** * ResourceValidator constructor * @param {*} concerto - the Concerto instance used for validation * @param {Object} [options] - the optional validation options. * @param {boolean} [options.validate] - validate the structure of the Resource * with its model prior to serialization (default to true) * @param {boolean} [options.convertResourcesToRelationships] - Convert resources that * are specified for relationship fields into relationships, false by default. * @param {boolean} [options.permitResourcesForRelationships] - Permit resources in the */ constructor(concerto, options) { this.options = options || {}; this.concerto = concerto; if(!this.concerto) { throw new Error('Missing concerto instance'); } } /** * Visitor design pattern. * * @param {Object} thing - the object being visited * @param {Object} parameters - the parameter * @return {Object} the result of visiting or null * @private */ visit(thing, parameters) { if (thing.isEnum?.()) { return this.visitEnumDeclaration(thing, parameters); } else if (thing.isClassDeclaration?.()) { return this.visitClassDeclaration(thing, parameters); } else if (thing.isRelationship?.()) { return this.visitRelationshipDeclaration(thing, parameters); } else if (thing.isTypeScalar?.()) { return this.visitField(thing.getScalarField(), parameters); } else if (thing.isField?.()) { return this.visitField(thing, parameters); } } /** * Visitor design pattern * * @param {EnumDeclaration} enumDeclaration - the object being visited * @param {Object} parameters - the parameter * @return {Object} the result of visiting or null * @private */ visitEnumDeclaration(enumDeclaration, parameters) { const obj = parameters.stack.pop(); // now check that obj is one of the enum values const properties = enumDeclaration.getProperties(); let found = false; for(let n=0; n < properties.length; n++) { const property = properties[n]; if(property.getName() === obj) { found = true; } } if(!found) { ObjectValidator.reportInvalidEnumValue( parameters.rootResourceIdentifier, enumDeclaration, obj ); } return null; } /** * Visitor design pattern * @param {ClassDeclaration} classDeclaration - the object being visited * @param {Object} parameters - the parameter * @return {Object} the result of visiting or null * @private */ visitClassDeclaration(classDeclaration, parameters) { const obj = parameters.stack.pop(); if(this.concerto.isIdentifiable(obj)) { parameters.rootResourceIdentifier = this.concerto.getFullyQualifiedIdentifier(obj); } const toBeAssignedClassDeclaration = this.concerto.getModelManager().getType(obj.$class); const toBeAssignedClassDecName = toBeAssignedClassDeclaration.getFullyQualifiedName(); // is the type we are assigning to abstract? // the only way this can happen is if the type is non-abstract // and then gets redeclared as abstract if(toBeAssignedClassDeclaration.isAbstract()) { ObjectValidator.reportAbstractClass(toBeAssignedClassDeclaration); } // are there extra fields in the object? let props = Object.getOwnPropertyNames(obj); for (let n = 0; n < props.length; n++) { let propName = props[n]; if(!this.isSystemProperty(propName)) { const field = toBeAssignedClassDeclaration.getProperty(propName); if (!field) { if(this.concerto.isIdentifiable(obj)) { ObjectValidator.reportUndeclaredField(this.concerto.getIdentifier(obj), propName, toBeAssignedClassDecName); } else { ObjectValidator.reportUndeclaredField(parameters.currentIdentifier, propName, toBeAssignedClassDecName); } } } } if (this.concerto.isIdentifiable( obj )) { const id = this.concerto.getIdentifier(obj); // prevent empty identifiers if(!id || id.trim().length === 0) { ObjectValidator.reportEmptyIdentifier(parameters.rootResourceIdentifier); } parameters.currentIdentifier = this.concerto.getFullyQualifiedIdentifier(obj); } // now validate each property const properties = toBeAssignedClassDeclaration.getProperties(); for(let n=0; n < properties.length; n++) { const property = properties[n]; const value = obj[property.getName()]; if(!Util.isNull(value)) { parameters.stack.push(value); property.accept(this,parameters); } else { // Should allow systems properties like $identifier if(!property.isOptional() && !this.isSystemProperty(property.getName())) { ObjectValidator.reportMissingRequiredProperty( parameters.rootResourceIdentifier, property); } } } return null; } /** * Returns true if the property is a system property. * System properties are not declared in the model. * @param {String} propertyName - the name of the property * @return {Boolean} true if the property is a system property * @private */ isSystemProperty(propertyName) { return propertyName.charAt(0) === '$'; } /** * Visitor design pattern * @param {Field} field - the object being visited * @param {Object} parameters - the parameter * @return {Object} the result of visiting or null * @private */ visitField(field, parameters) { const obj = parameters.stack.pop(); let dataType = typeof(obj); let propName = field.getName(); if (dataType === 'symbol') { ObjectValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, propName, obj, field, this.concerto); } if(field.isTypeEnum()) { this.checkEnum(obj, field,parameters); } else { if(field.isArray()) { this.checkArray(obj, field,parameters); } else { this.checkItem(obj, field,parameters); } } return null; } /** * Check a Field that is declared as an Array. * @param {Object} obj - the object being validated * @param {Field} field - the object being visited * @param {Object} parameters - the parameter * @private */ checkEnum(obj,field,parameters) { if(field.isArray() && !(obj instanceof Array)) { ObjectValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, field.getName(), obj, field, this.concerto); } const enumDeclaration = field.getParent().getModelFile().getType(field.getType()); if(field.isArray()) { for(let n=0; n < obj.length; n++) { const item = obj[n]; parameters.stack.push(item); enumDeclaration.accept(this, parameters); } } else { const item = obj; parameters.stack.push(item); enumDeclaration.accept(this, parameters); } } /** * Check a Field that is declared as an Array. * @param {Object} obj - the object being validated * @param {Field} field - the object being visited * @param {Object} parameters - the parameter * @private */ checkArray(obj,field,parameters) { if(!(obj instanceof Array)) { ObjectValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, field.getName(), obj, field, this.concerto); } for(let n=0; n < obj.length; n++) { const item = obj[n]; this.checkItem(item, field, parameters); } } /** * Check a single (non-array) field. * @param {Object} obj - the object being validated * @param {Field} field - the object being visited * @param {Object} parameters - the parameter * @private */ checkItem(obj,field, parameters) { let dataType = typeof obj; let propName = field.getName(); if (dataType === 'symbol') { ObjectValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, propName, obj, field, this.concerto); } if(field.isPrimitive()) { let invalid = false; switch(field.getType()) { case 'String': if(dataType !== 'string') { invalid = true; } break; case 'Double': case 'Long': case 'Integer': if(dataType !== 'number') { invalid = true; } break; case 'Boolean': if(dataType !== 'boolean') { invalid = true; } break; case 'DateTime': if(typeof obj !== 'string' || !dayjs.utc(obj).isValid()) { invalid = true; } break; } if (invalid) { ObjectValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, propName, obj, field, this.concerto); } else { if(field.getValidator() !== null) { field.getValidator().validate(parameters.currentIdentifier, obj); } } } else { // a field that points to a transaction, asset, participant... let classDeclaration = this.concerto.getModelManager().getType(field.getFullyQualifiedTypeName()); try { classDeclaration = this.concerto.getModelManager().getType(obj.$class); } catch (err) { ObjectValidator.reportFieldTypeViolation(parameters.rootResourceIdentifier, propName, obj, field, this.concerto); } // is it compatible? if(!ModelUtil.isAssignableTo(classDeclaration.getModelFile(), classDeclaration.getFullyQualifiedName(), field)) { ObjectValidator.reportInvalidFieldAssignment(parameters.rootResourceIdentifier, propName, obj, field); } // recurse parameters.stack.push(obj); classDeclaration.accept(this, parameters); } } /** * Visitor design pattern * @param {RelationshipDeclaration} relationshipDeclaration - the object being visited * @param {Object} parameters - the parameter * @return {Object} the result of visiting or null * @private */ visitRelationshipDeclaration(relationshipDeclaration, parameters) { const obj = parameters.stack.pop(); if(relationshipDeclaration.isArray()) { if(!(obj instanceof Array)) { ObjectValidator.reportInvalidFieldAssignment(parameters.rootResourceIdentifier, relationshipDeclaration.getName(), obj, relationshipDeclaration); } for(let n=0; n < obj.length; n++) { const item = obj[n]; this.checkRelationship(parameters, relationshipDeclaration, item); } } else { this.checkRelationship(parameters, relationshipDeclaration, obj); } return null; } /** * Check a single relationship * @param {Object} parameters - the parameter * @param {*} relationshipDeclaration - the object being visited * @param {Object} obj - the object being validated * @private */ checkRelationship(parameters, relationshipDeclaration, obj) { if(this.concerto.isRelationship( obj )) { // All good.. } else if (this.concerto.isIdentifiable(obj) && (this.options.convertResourcesToRelationships || this.options.permitResourcesForRelationships)) { // All good.. Again } else { ObjectValidator.reportNotRelationshipViolation(parameters.rootResourceIdentifier, relationshipDeclaration, obj); } const relationshipType = this.concerto.isRelationship(obj) ? this.concerto.fromURI(obj).typeDeclaration : this.concerto.getTypeDeclaration(obj); if(!relationshipType.getIdentifierFieldName()) { throw new Error('Relationship can only be to identified types.'); } if(!ModelUtil.isAssignableTo(relationshipType.getModelFile(), relationshipType.getFullyQualifiedName(), relationshipDeclaration)) { ObjectValidator.reportInvalidFieldAssignment(parameters.rootResourceIdentifier, relationshipDeclaration.getName(), obj, relationshipDeclaration); } } /** * Throw a new error for a model violation. * @param {string} id - the identifier of this instance. * @param {string} propName - the name of the field. * @param {*} value - the value of the field. * @param {Field} field - the field * @param {*} concerto - the concerto instance * @throws {ValidationException} the exception * @private */ static reportFieldTypeViolation(id, propName, value, field, concerto) { let isArray = field.isArray() ? '[]' : ''; let typeOfValue = typeof value; if( concerto.isObject(value) && concerto.isIdentifiable(value)) { typeOfValue = concerto.getType(value); value = concerto.getIdentifier(value); } else { if(value) { try { value = JSON.stringify(value); } catch(err) { value = value.toString(); } } } let formatter = Globalize.messageFormatter('resourcevalidator-fieldtypeviolation'); throw new ValidationException(formatter({ resourceId: id, propertyName: propName, fieldType: field.getType() + isArray, value: value, typeOfValue: typeOfValue })); } /** * Throw a new error for a model violation. * @param {string} id - the identifier of this instance. * @param {RelationshipDeclaration} relationshipDeclaration - the declaration of the classs * @param {Object} value - the value of the field. * @private */ static reportNotRelationshipViolation(id, relationshipDeclaration, value) { let formatter = Globalize.messageFormatter('resourcevalidator-notrelationship'); throw new ValidationException(formatter({ resourceId: id, classFQN: relationshipDeclaration.getFullyQualifiedTypeName(), invalidValue: value.toString() })); } /** * Throw a new error for a missing, but required field. * @param {string} id - the identifier of this instance. * @param {Field} field - the field/ * @private */ static reportMissingRequiredProperty(id, field) { let formatter = Globalize.messageFormatter('resourcevalidator-missingrequiredproperty'); throw new ValidationException(formatter({ resourceId: id, fieldName: field.getName() })); } /** * Throw a new error for a missing, but required field. * @param {string} id - the identifier of this instance. * @param {Field} field - the field/ * @private */ static reportEmptyIdentifier(id) { let formatter = Globalize.messageFormatter('resourcevalidator-emptyidentifier'); throw new ValidationException(formatter({ resourceId: id })); } /** * Throw a new error for a missing, but required field. * @param {string} id - the identifier of this instance. * @param {Field} field - the field * @param {string} obj - the object value * @private */ static reportInvalidEnumValue(id, field, obj) { let formatter = Globalize.messageFormatter('resourcevalidator-invalidenumvalue'); throw new ValidationException(formatter({ resourceId: id, value: obj, fieldName: field.getName() })); } /** * Throw a validation exception for an abstract class * @param {ClassDeclaration} classDeclaration - the class declaration * @throws {ValidationException} the validation exception * @private */ static reportAbstractClass(classDeclaration) { let formatter = Globalize.messageFormatter('resourcevalidator-abstractclass'); throw new ValidationException(formatter({ className: classDeclaration.getFullyQualifiedName(), })); } /** * Throw a validation exception for an abstract class * @param {string} resourceId - the id of the resouce being validated * @param {string} propertyName - the name of the property that is not declared * @param {string} fullyQualifiedTypeName - the fully qualified type being validated * @throws {ValidationException} the validation exception * @private */ static reportUndeclaredField(resourceId, propertyName, fullyQualifiedTypeName ) { let formatter = Globalize.messageFormatter('resourcevalidator-undeclaredfield'); throw new ValidationException(formatter({ resourceId: resourceId, propertyName: propertyName, fullyQualifiedTypeName: fullyQualifiedTypeName })); } /** * Throw a validation exception for an invalid field assignment * @param {string} resourceId - the id of the resouce being validated * @param {string} propName - the name of the property that is being assigned * @param {*} obj - the Field * @param {Field} field - the Field * @throws {ValidationException} the validation exception * @private */ static reportInvalidFieldAssignment(resourceId, propName, obj, field) { let formatter = Globalize.messageFormatter('resourcevalidator-invalidfieldassignment'); let typeName = field.getFullyQualifiedTypeName(); if(field.isArray()) { typeName += '[]'; } throw new ValidationException(formatter({ resourceId: resourceId, propertyName: propName, objectType: obj.$class, fieldType: typeName })); } } module.exports = ObjectValidator;