Skip to content

Commit bcdbd34

Browse files
authored
feat(decorators): validate decorators against model (#916)
* feat(decorators): validate decorators against model Signed-off-by: Dan Selman <danscode@selman.org>
1 parent b8c465f commit bcdbd34

30 files changed

+870
-215
lines changed

packages/concerto-core/api.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ class AstModelManager extends BaseModelManager {
22
+ void constructor(object?)
33
}
44
class BaseModelManager {
5-
+ void constructor(object?,boolean?,Object?,boolean?,boolean?,boolean?,boolean?,processFile?)
5+
+ void constructor(object?,boolean?,Object?,boolean?,boolean?,boolean?,boolean?,object?,string?,string?,processFile?)
66
+ boolean isModelManager()
77
+ boolean isStrict()
88
+ boolean isAliasedTypeEnabled()
@@ -16,6 +16,7 @@ class BaseModelManager {
1616
+ void validateModelFiles()
1717
+ Promise updateExternalModels(Object?,FileDownloader?) throws IllegalModelException
1818
+ void writeModelsToFileSystem(string,Object?,boolean)
19+
+ object getDecoratorValidation()
1920
+ Object[] getModels(Object?,boolean)
2021
+ void clearModelFiles()
2122
+ ModelFile getModelFile(string)
@@ -35,7 +36,7 @@ class BaseModelManager {
3536
+ boolean derivesFrom(string,string)
3637
+ object resolveMetaModel(object)
3738
+ void fromAst(ast)
38-
+ void getAst(boolean?)
39+
+ void getAst(boolean?,boolean?)
3940
+ BaseModelManager filter(FilterFunction)
4041
}
4142
class Concerto {
@@ -57,7 +58,7 @@ class Concerto {
5758
+ object setCurrentTime()
5859
class DecoratorManager {
5960
+ ModelManager validate(decoratorCommandSet,ModelFile[]) throws Error
60-
+ ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?,boolean?,boolean?)
61+
+ ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?,boolean?,boolean?,boolean?)
6162
+ ExtractDecoratorsResult extractDecorators(ModelManager,object,boolean,string)
6263
+ ExtractDecoratorsResult extractVocabularies(ModelManager,object,boolean,string)
6364
+ ExtractDecoratorsResult extractNonVocabDecorators(ModelManager,object,boolean,string)
@@ -69,6 +70,7 @@ class DecoratorManager {
6970
}
7071
+ string[] intersect()
7172
+ boolean isUnversionedNamespaceEqual()
73+
+ object getDecoratorModel()
7274
class Factory {
7375
+ string newId()
7476
+ void constructor(ModelManager)

packages/concerto-core/changelog.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
# Note that the latest public API is documented using JSDocs and is available in api.txt.
2525
#
2626

27+
Version 3.19.2 {56bc38dc7305eee0a06f08a8cf639910} 2024-10-02
28+
- validateDecorators option added to ModelManager
29+
- update DecoratorManager to support validated decorators
30+
2731
Version 3.17.5 {9bd69f9522c14a99a085f077e12ac4b2} 2024-08-29
2832
- importAliasing added to ModelManager parameters
2933

packages/concerto-core/lib/basemodelmanager.js

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const ModelUtil = require('./modelutil');
2727
const Serializer = require('./serializer');
2828
const TypeNotFoundException = require('./typenotfoundexception');
2929
const { getRootModel } = require('./rootmodel');
30+
const { getDecoratorModel } = require('./decoratormodel');
3031
const MetamodelException = require('./metamodelexception');
3132

3233
// Types needed for TypeScript generation.
@@ -57,6 +58,16 @@ const defaultProcessFile = (name, data) => {
5758
};
5859
};
5960

61+
// default decorator validation configuration
62+
const DEFAULT_DECORATOR_VALIDATION = {
63+
missingDecorator: undefined, // 'error' | 'warn' (see Logger.levels)...,
64+
invalidDecorator: undefined, // 'error' | 'warn' ...
65+
};
66+
67+
// these namespaces are internal and excluded by default by getModelFiles
68+
// and ignored by fromAst
69+
const EXCLUDE_NS = ['concerto@1.0.0', 'concerto', 'concerto.decorator@1.0.0'];
70+
6071
/**
6172
* Manages the Concerto model files.
6273
*
@@ -83,6 +94,9 @@ class BaseModelManager {
8394
* @param {boolean} [options.addMetamodel] - When true, the Concerto metamodel is added to the model manager
8495
* @param {boolean} [options.enableMapType] - When true, the Concerto Map Type feature is enabled
8596
* @param {boolean} [options.importAliasing] - When true, the Concerto Aliasing feature is enabled
97+
* @param {object} [options.decoratorValidation] - the decorator validation configuration
98+
* @param {string} [options.decoratorValidation.missingDecorator] - the validation log level for missingDecorator decorators: off, warning, error
99+
* @param {string} [options.decoratorValidation.invalidDecorator] - the validation log level for invalidDecorator decorators: off, warning, error
86100
* @param {*} [processFile] - how to obtain a concerto AST from an input to the model manager
87101
*/
88102
constructor(options, processFile) {
@@ -93,12 +107,15 @@ class BaseModelManager {
93107
this.decoratorFactories = [];
94108
this.strict = !!options?.strict;
95109
this.options = options;
110+
this.addDecoratorModel();
96111
this.addRootModel();
112+
this.decoratorValidation = options?.decoratorValidation ? options?.decoratorValidation : DEFAULT_DECORATOR_VALIDATION;
97113

98114
// TODO Remove on release of MapType
99115
// Supports both env var and property based flag
100116
this.enableMapType = !!options?.enableMapType;
101117
this.importAliasing = process?.env?.IMPORT_ALIASING === 'true' || !!options?.importAliasing;
118+
102119
// Cache a copy of the Metamodel ModelFile for use when validating the structure of ModelFiles later.
103120
this.metamodelModelFile = new ModelFile(this, MetaModelUtil.metaModelAst, undefined, MetaModelNamespace);
104121

@@ -155,6 +172,16 @@ class BaseModelManager {
155172
}
156173
}
157174

175+
/**
176+
* Adds decorator types
177+
* @private
178+
*/
179+
addDecoratorModel() {
180+
const {decoratorModelAst, decoratorModelCto, decoratorModelFile} = getDecoratorModel();
181+
const m = new ModelFile(this, decoratorModelAst, decoratorModelCto, decoratorModelFile);
182+
this.addModelFile(m, decoratorModelCto, decoratorModelFile, true);
183+
}
184+
158185
/**
159186
* Visitor design pattern
160187
* @param {Object} visitor - the visitor
@@ -469,6 +496,14 @@ class BaseModelManager {
469496
ModelWriter.writeModelsToFileSystem(this.getModelFiles(), path, options);
470497
}
471498

499+
/**
500+
* Returns the status of the decorator validation options
501+
* @returns {object} returns an object that indicates the log levels for defined and undefined decorators
502+
*/
503+
getDecoratorValidation() {
504+
return this.decoratorValidation;
505+
}
506+
472507
/**
473508
* Get the array of model file instances
474509
* @param {Boolean} [includeConcertoNamespace] - whether to include the concerto namespace
@@ -482,7 +517,7 @@ class BaseModelManager {
482517

483518
for (let n = 0; n < keys.length; n++) {
484519
const ns = keys[n];
485-
if(includeConcertoNamespace || (ns !== 'concerto@1.0.0' && ns !== 'concerto')) {
520+
if(includeConcertoNamespace || (!EXCLUDE_NS.includes(ns))) {
486521
result.push(this.modelFiles[ns]);
487522
}
488523
}
@@ -561,6 +596,7 @@ class BaseModelManager {
561596
*/
562597
clearModelFiles() {
563598
this.modelFiles = {};
599+
this.addDecoratorModel();
564600
this.addRootModel();
565601
}
566602

@@ -751,7 +787,7 @@ class BaseModelManager {
751787
* @return {object} the resolved metamodel
752788
*/
753789
resolveMetaModel(metaModel) {
754-
const priorModels = this.getAst();
790+
const priorModels = this.getAst(false, true);
755791
return MetaModelUtil.resolveLocalNames(priorModels, metaModel);
756792
}
757793

@@ -762,23 +798,26 @@ class BaseModelManager {
762798
fromAst(ast) {
763799
this.clearModelFiles();
764800
ast.models.forEach( model => {
765-
const modelFile = new ModelFile( this, model );
766-
this.addModelFile( modelFile, null, null, true );
801+
if(!EXCLUDE_NS.includes(model.namespace)) { // excludes the internal namespaces, already added
802+
const modelFile = new ModelFile( this, model );
803+
this.addModelFile( modelFile, null, null, true );
804+
}
767805
});
768806
this.validateModelFiles();
769807
}
770808

771809
/**
772810
* Get the full ast (metamodel instances) for a modelmanager
773811
* @param {boolean} [resolve] - whether to resolve names
812+
* @param {boolean} [includeConcertoNamespaces] - whether to include the concerto namespaces
774813
* @returns {*} the metamodel
775814
*/
776-
getAst(resolve) {
815+
getAst(resolve,includeConcertoNamespaces) {
777816
const result = {
778817
$class: `${MetaModelNamespace}.Models`,
779818
models: [],
780819
};
781-
const modelFiles = this.getModelFiles();
820+
const modelFiles = this.getModelFiles(includeConcertoNamespaces);
782821
modelFiles.forEach((thisModelFile) => {
783822
let metaModel = thisModelFile.getAst();
784823
if (resolve) {

packages/concerto-core/lib/decoratormanager.js

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ if (global === undefined) {
3131
}
3232
/* eslint-enable no-unused-vars */
3333

34-
const DCS_VERSION = '0.3.0';
34+
const DCS_VERSION = '0.4.0';
3535

3636
const DCS_MODEL = `concerto version "^3.0.0"
37-
namespace org.accordproject.decoratorcommands@0.3.0
37+
namespace org.accordproject.decoratorcommands@0.4.0
3838
3939
import concerto.metamodel@1.0.0.Decorator
4040
@@ -83,6 +83,7 @@ concept Command {
8383
o CommandTarget target
8484
o Decorator decorator
8585
o CommandType type
86+
o String decoratorNamespace optional
8687
}
8788
8889
/**
@@ -325,7 +326,7 @@ class DecoratorManager {
325326
validationModelManager.addModelFiles(modelManager.getModelFiles());
326327
validationModelManager.addCTOModel(
327328
DCS_MODEL,
328-
'decoratorcommands@0.3.0.cto'
329+
'decoratorcommands@0.4.0.cto'
329330
);
330331
const factory = new Factory(validationModelManager);
331332
const serializer = new Serializer(factory, validationModelManager);
@@ -365,17 +366,39 @@ class DecoratorManager {
365366
* @param {boolean} [options.validateCommands] - validate the decorator command set targets. Note that
366367
* the validate option must also be true
367368
* @param {boolean} [options.migrate] - migrate the decoratorCommandSet $class to match the dcs model version
369+
* @param {boolean} [options.defaultNamespace] - the default namespace to use for decorator commands that include a decorator without a namespace
368370
* @param {boolean} [options.enableDcsNamespaceTarget] - flag to control applying namespace targeted decorators on top of the namespace instead of all declarations in that namespace
369371
* @returns {ModelManager} a new model manager with the decorations applied
370372
*/
371373
static decorateModels(modelManager, decoratorCommandSet, options) {
372374

373375
this.migrateAndValidate(modelManager, decoratorCommandSet, options?.migrate, options?.validate, options?.validateCommands);
374376

377+
// we create synthetic imports for all decorator declarations
378+
// along with any of their type reference arguments
379+
const decoratorImports = decoratorCommandSet.commands.map(command => {
380+
return [{
381+
$class: `${MetaModelNamespace}.ImportType`,
382+
name: command.decorator.name,
383+
namespace: command.decorator.namespace ? command.decorator.namespace : options?.defaultNamespace
384+
}].concat(command.decorator.arguments ? command.decorator.arguments?.filter(a => a.type)
385+
.map(a => {
386+
return {
387+
$class: `${MetaModelNamespace}.ImportType`,
388+
name: a.type.name,
389+
namespace: a.type.namespace ? a.type.namespace : options?.defaultNamespace
390+
};
391+
})
392+
: []);
393+
}).flat().filter(i => i.namespace);
375394
const { namespaceCommandsMap, declarationCommandsMap, propertyCommandsMap, mapElementCommandsMap, typeCommandsMap } = this.getDecoratorMaps(decoratorCommandSet);
376-
const ast = modelManager.getAst(true);
395+
const ast = modelManager.getAst(true, true);
377396
const decoratedAst = JSON.parse(JSON.stringify(ast));
378397
decoratedAst.models.forEach((model) => {
398+
// remove the imports for types defined in this namespace
399+
const neededImports = decoratorImports.filter(i => i.namespace !== model.namespace);
400+
// add the imports for decorators, in case they get added below
401+
model.imports = model.imports ? model.imports.concat(neededImports) : neededImports;
379402
model.declarations.forEach((decl) => {
380403
const declarationDecoratorCommandSets = [];
381404
const { name: declarationName, $class: $classForDeclaration } = decl;
@@ -421,8 +444,12 @@ class DecoratorManager {
421444

422445
});
423446
});
447+
424448
const enableMapType = modelManager?.enableMapType ? true : false;
425-
const newModelManager = new ModelManager({ enableMapType });
449+
const newModelManager = new ModelManager({
450+
strict: modelManager.isStrict(),
451+
enableMapType,
452+
decoratorValidation: modelManager.getDecoratorValidation()});
426453
newModelManager.fromAst(decoratedAst);
427454
return newModelManager;
428455
}
@@ -447,7 +474,7 @@ class DecoratorManager {
447474
locale:'en',
448475
...options
449476
};
450-
const sourceAst = modelManager.getAst(true);
477+
const sourceAst = modelManager.getAst(true, true);
451478
const decoratorExtrator = new DecoratorExtractor(options.removeDecoratorsFromModel, options.locale, DCS_VERSION, sourceAst, DecoratorExtractor.Action.EXTRACT_ALL);
452479
const collectionResp = decoratorExtrator.extract();
453480
return {
@@ -470,7 +497,7 @@ class DecoratorManager {
470497
locale:'en',
471498
...options
472499
};
473-
const sourceAst = modelManager.getAst(true);
500+
const sourceAst = modelManager.getAst(true, true);
474501
const decoratorExtrator = new DecoratorExtractor(options.removeDecoratorsFromModel, options.locale, DCS_VERSION, sourceAst, DecoratorExtractor.Action.EXTRACT_VOCAB);
475502
const collectionResp = decoratorExtrator.extract();
476503
return {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
'use strict';
16+
17+
/** @type unknown */
18+
const decoratorModelAst = require('./decoratormodel.json');
19+
20+
/**
21+
* Gets the decorator 'concerto.decorator' model
22+
* @returns {object} decoratorModelFile, decoratorModelCto and decoratorModelAst
23+
*/
24+
function getDecoratorModel() {
25+
const decoratorModelFile = 'concerto_decorator_1.0.0.cto';
26+
const decoratorModelCto = `namespace concerto.decorator@1.0.0
27+
abstract concept Decorator {}
28+
concept DotNetNamespace extends Decorator {
29+
o String namespace
30+
}`;
31+
const ast = JSON.parse(JSON.stringify(decoratorModelAst));
32+
return { decoratorModelFile, decoratorModelCto, decoratorModelAst: ast };
33+
}
34+
35+
module.exports = { getDecoratorModel };
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$class": "concerto.metamodel@1.0.0.Model",
3+
"decorators": [],
4+
"namespace": "concerto.decorator@1.0.0",
5+
"imports": [],
6+
"declarations": [
7+
{
8+
"$class": "concerto.metamodel@1.0.0.ConceptDeclaration",
9+
"name": "Decorator",
10+
"isAbstract": true,
11+
"properties": []
12+
},
13+
{
14+
"$class": "concerto.metamodel@1.0.0.ConceptDeclaration",
15+
"name": "DotNetNamespace",
16+
"superType": {
17+
"$class": "concerto.metamodel@1.0.0.TypeIdentifier",
18+
"name": "Decorator"
19+
},
20+
"isAbstract": false,
21+
"properties": [
22+
{
23+
"$class": "concerto.metamodel@1.0.0.StringProperty",
24+
"name": "namespace",
25+
"isArray": false,
26+
"isOptional": false
27+
}
28+
]
29+
}
30+
]
31+
}

0 commit comments

Comments
 (0)