Skip to content

Commit

Permalink
feat(decorators): validate decorators against model
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Selman <danscode@selman.org>
  • Loading branch information
dselman committed Oct 2, 2024
1 parent 25d555a commit a5044ac
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 6 deletions.
3 changes: 2 additions & 1 deletion packages/concerto-core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class AstModelManager extends BaseModelManager {
+ void constructor(object?)
}
class BaseModelManager {
+ void constructor(object?,boolean?,Object?,boolean?,boolean?,boolean?,boolean?,processFile?)
+ void constructor(object?,boolean?,Object?,boolean?,boolean?,boolean?,boolean?,object?,string?,string?,processFile?)
+ boolean isModelManager()
+ boolean isStrict()
+ boolean isAliasedTypeEnabled()
Expand All @@ -16,6 +16,7 @@ class BaseModelManager {
+ void validateModelFiles()
+ Promise updateExternalModels(Object?,FileDownloader?) throws IllegalModelException
+ void writeModelsToFileSystem(string,Object?,boolean)
+ object getDecoratorValidation()
+ Object[] getModels(Object?,boolean)
+ void clearModelFiles()
+ ModelFile getModelFile(string)
Expand Down
3 changes: 3 additions & 0 deletions packages/concerto-core/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
# Note that the latest public API is documented using JSDocs and is available in api.txt.
#

Version 3.19.2 {0c20619546430976288fd73984666c4a} 2024-10-02
- validateDecorators option added to ModelManager

Version 3.17.5 {9bd69f9522c14a99a085f077e12ac4b2} 2024-08-29
- importAliasing added to ModelManager parameters

Expand Down
23 changes: 23 additions & 0 deletions packages/concerto-core/lib/basemodelmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ const defaultProcessFile = (name, data) => {
};
};

// default decorator validation configuration
const DEFAULT_DECORATOR_VALIDATION = {
missingDecorator: undefined, // 'error' | 'warn' (see Logger.levels)...,
invalidDecorator: undefined, // 'error' | 'warn' ...
};

/**
* Manages the Concerto model files.
*
Expand All @@ -83,6 +89,9 @@ class BaseModelManager {
* @param {boolean} [options.addMetamodel] - When true, the Concerto metamodel is added to the model manager
* @param {boolean} [options.enableMapType] - When true, the Concerto Map Type feature is enabled
* @param {boolean} [options.importAliasing] - When true, the Concerto Aliasing feature is enabled
* @param {object} [options.decoratorValidation] - the decorator validation configuration
* @param {string} [options.decoratorValidation.defined] - the validation log level for defined decorators: off, warning, error
* @param {string} [options.decoratorValidation.undefined] - the validation log level for undefined decorators: off, warning, error
* @param {*} [processFile] - how to obtain a concerto AST from an input to the model manager
*/
constructor(options, processFile) {
Expand All @@ -94,11 +103,14 @@ class BaseModelManager {
this.strict = !!options?.strict;
this.options = options;
this.addRootModel();
this.decorators = undefined;
this.decoratorValidation = options?.decoratorValidation ? options?.decoratorValidation : DEFAULT_DECORATOR_VALIDATION;

// TODO Remove on release of MapType
// Supports both env var and property based flag
this.enableMapType = !!options?.enableMapType;
this.importAliasing = process?.env?.IMPORT_ALIASING === 'true' || !!options?.importAliasing;

// Cache a copy of the Metamodel ModelFile for use when validating the structure of ModelFiles later.
this.metamodelModelFile = new ModelFile(this, MetaModelUtil.metaModelAst, undefined, MetaModelNamespace);

Expand Down Expand Up @@ -344,6 +356,7 @@ class BaseModelManager {
if (!this.modelFiles[namespace]) {
throw new Error('Model file does not exist');
} else {
this.decorators = undefined;
delete this.modelFiles[namespace];
}
}
Expand Down Expand Up @@ -407,6 +420,8 @@ class BaseModelManager {
* Validates all models files in this model manager
*/
validateModelFiles() {
// clear the decorators, because the model files may have changed
this.decorators = undefined;
for (let ns in this.modelFiles) {
this.modelFiles[ns].validate();
}
Expand Down Expand Up @@ -469,6 +484,14 @@ class BaseModelManager {
ModelWriter.writeModelsToFileSystem(this.getModelFiles(), path, options);
}

/**
* Returns the status of the decorator validation options
* @returns {object} returns an object that indicates the log levels for defined and undefined decorators
*/
getDecoratorValidation() {
return this.decoratorValidation;
}

/**
* Get the array of model file instances
* @param {Boolean} [includeConcertoNamespace] - whether to include the concerto namespace
Expand Down
79 changes: 77 additions & 2 deletions packages/concerto-core/lib/introspect/decorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
'use strict';

const { MetaModelNamespace } = require('@accordproject/concerto-metamodel');
const { Logger } = require('@accordproject/concerto-util');

// Types needed for TypeScript generation.
/* eslint-disable no-unused-vars */
Expand All @@ -25,6 +26,19 @@ if (global === undefined) {
}
/* eslint-enable no-unused-vars */

/**
* Handles a validation error, logging and throwing as required
* @param {string} level the log level
* @param {*} err the error to log
* @private
*/
function handleError(level, err) {
Logger.dispatch(level, err);
if(level === 'error') {
throw err;
}
}

/**
* Decorator encapsulates a decorator (annotation) on a class or property.
* @class
Expand Down Expand Up @@ -91,15 +105,76 @@ class Decorator {
}

/**
* Validate the property
* Validate the decorator
* @throws {IllegalModelException}
* @private
*/
validate() {
const mf = this.getParent().getModelFile();
const mm = mf.getModelManager();
const validationOptions = mm.getDecoratorValidation();

if(validationOptions.missingDecorator || validationOptions.invalidDecorator) {
try {
// this throws if the type does not exist
mf.resolveType(`Decorator ${this.getName()} on ${this.getParent().getName()}`, this.getName());
const decoratorDecl = mf.getType(this.getName());
const requiredProperties = decoratorDecl.getProperties().filter(p => !p.isOptional());
const optionalProperties = decoratorDecl.getProperties().filter(p => p.isOptional());
const allProperties = [...requiredProperties, ...optionalProperties];
if(this.getArguments().length < requiredProperties.length) {
const err = `Decorator ${this.getName()} has too few arguments. Required properties are: [${requiredProperties.map(p => p.getName()).join()}]`;
handleError(validationOptions.invalidDecorator, err);
}
const args = this.getArguments();
for (let n = 0; n < args.length; n++) {
const arg = args[n];
if (n > allProperties.length-1) {
const err = `Decorator ${this.getName()} has too many arguments. Properties are: [${allProperties.map(p => p.getName()).join()}]`;
handleError(validationOptions.invalidDecorator, err);
}
else {
const property = allProperties[n];
const argType = typeof arg;
switch (property.getType()) {
case 'Integer':
case 'Double':
case 'Long':
if (argType !== 'number') {
const err = `Decorator ${this.getName()} has invalid decorator argument. Expected number. Found ${argType}, with value ${JSON.stringify(arg)}`;
handleError(validationOptions.invalidDecorator, err);
}
break;
case 'String':
if (argType !== 'string') {
const err = `Decorator ${this.getName()} has invalid decorator argument. Expected string. Found ${argType}, with value ${JSON.stringify(arg)}`;
handleError(validationOptions.invalidDecorator, err);
}
break;
case 'Boolean':
if (argType !== 'boolean') {
const err = `Decorator ${this.getName()} has invalid decorator argument. Expected boolean. Found ${argType}, with value ${JSON.stringify(arg)}`;
handleError(validationOptions.invalidDecorator, err);
}
break;
default: {
if (argType !== 'object') {
const err = `Decorator ${this.getName()} has invalid decorator argument. Expected object. Found ${argType}, with value ${JSON.stringify(arg)}`;
handleError(validationOptions.invalidDecorator, err);
}
break;
}
}
}
}
}
catch(err) {
handleError(validationOptions.missingDecorator, err);
}
}
// check that all type ref arguments can be resolved
const typeRefs = this.arguments.filter(a => a?.type === 'Identifier');
typeRefs.forEach(typeRef => {
const mf = this.getParent().getModelFile();
mf.resolveType(`Decorator ${this.getName()} on ${this.getParent().getName()}`, typeRef.name);
});
}
Expand Down
5 changes: 4 additions & 1 deletion packages/concerto-core/lib/rootmodel.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ function getRootModel(versioned) {
abstract concept Participant identified {}
abstract concept Transaction {}
abstract concept Event {}
`;
abstract concept Decorator {}
concept DotNetNamespace extends Decorator {
o String namespace
}`;
const ast = JSON.parse(JSON.stringify(rootModelAst));
ast.namespace = ns;
return { rootModelFile, rootModelCto, rootModelAst: ast };
Expand Down
25 changes: 24 additions & 1 deletion packages/concerto-core/lib/rootmodel.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,29 @@
"name": "Event",
"isAbstract": true,
"properties": []
},
{
"$class": "concerto.metamodel@1.0.0.ConceptDeclaration",
"name": "Decorator",
"isAbstract": true,
"properties": []
},
{
"$class": "concerto.metamodel@1.0.0.ConceptDeclaration",
"name": "DotNetNamespace",
"superType": {
"$class": "concerto.metamodel@1.0.0.TypeIdentifier",
"name": "Decorator"
},
"isAbstract": false,
"properties": [
{
"$class": "concerto.metamodel@1.0.0.StringProperty",
"name": "namespace",
"isArray": false,
"isOptional": false
}
]
}
]
}
}
2 changes: 1 addition & 1 deletion packages/concerto-core/test/modelmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@ concept Bar {
modelManager.getModelFile('org.acme').should.not.be.null;

// import all external models
return modelManager.updateExternalModels().should.be.rejectedWith(Error, 'Failed to load model file. Job: github://external.cto Details: Error: HTTP request failed with status: 400');
return modelManager.updateExternalModels().should.be.rejectedWith(Error, 'Unable to download external model dependency \'github://external.cto\'');
});

it('should fail using bad protocol and default model file loader', () => {
Expand Down
Loading

0 comments on commit a5044ac

Please sign in to comment.