diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 3a415fb38..c999ea922 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -56,7 +56,7 @@ class Concerto { + object setCurrentTime() class DecoratorManager { + ModelManager validate(decoratorCommandSet,ModelFile[]) throws Error - + ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?) + + ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?,boolean?) + void validateCommand(ModelManager,command) + Boolean falsyOrEqual(string||,string[]) + void applyDecorator(decorated,string,newDecorator) diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index 1f47d9773..6af538a7d 100644 --- a/packages/concerto-core/changelog.txt +++ b/packages/concerto-core/changelog.txt @@ -24,6 +24,9 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 3.13.1 {f5a9a1ea6a64865843a3abb77798cbb0} 2023-10-18 +- Add migrate option to DecoratorManager options + Version 3.13.1 {f435a20a00712e49c5cd32bc73ecb06a} 2023-10-03 - Add JSDoc for enableMapType option on ModelManager diff --git a/packages/concerto-core/lib/decoratormanager.js b/packages/concerto-core/lib/decoratormanager.js index 718b16d8a..ab684705f 100644 --- a/packages/concerto-core/lib/decoratormanager.js +++ b/packages/concerto-core/lib/decoratormanager.js @@ -18,6 +18,8 @@ const ModelManager = require('./modelmanager'); const Serializer = require('./serializer'); const Factory = require('./factory'); const ModelUtil = require('./modelutil'); +const { MetaModelNamespace } = require('@accordproject/concerto-metamodel'); +const semver = require('semver'); // Types needed for TypeScript generation. /* eslint-disable no-unused-vars */ @@ -27,6 +29,7 @@ if (global === undefined) { } /* eslint-enable no-unused-vars */ +const DCS_VERSION = '0.3.0'; const DCS_MODEL = `concerto version "^3.0.0" namespace org.accordproject.decoratorcommands@0.3.0 @@ -59,6 +62,16 @@ concept CommandTarget { o String property optional o String[] properties optional // property and properties are mutually exclusive o String type optional + o MapElement mapElement optional +} + +/** + * Map Declaration elements which might be used as a target + */ +enum MapElement { + o KEY + o VALUE + o KEY_VALUE } /** @@ -147,6 +160,43 @@ class DecoratorManager { return validationModelManager; } + /** + * Rewrites the $class property on decoratorCommandSet classes. + * @private + * @param {*} decoratorCommandSet the DecoratorCommandSet object + * @param {string} version the DCS version upgrade target + * @returns {object} the migrated DecoratorCommandSet object + */ + static migrateTo(decoratorCommandSet, version) { + if (decoratorCommandSet instanceof Object) { + for (let key in decoratorCommandSet) { + if (key === '$class' && decoratorCommandSet[key].includes('org.accordproject.decoratorcommands')) { + const ns = ModelUtil.getNamespace(decoratorCommandSet.$class); + decoratorCommandSet[key] = decoratorCommandSet[key].replace( + ModelUtil.parseNamespace(ns).version, + DCS_VERSION); + } + if (decoratorCommandSet[key] instanceof Object || decoratorCommandSet[key] instanceof Array) { + this.migrateTo(decoratorCommandSet[key], version); + } + } + } + return decoratorCommandSet; + } + + /** + * Checks if the supplied decoratorCommandSet can be migrated. + * Migrations should only take place across minor versions of the same major version. + * @private + * @param {*} decoratorCommandSet the DecoratorCommandSet object + * @param {*} DCS_VERSION the DecoratorCommandSet version + * @returns {boolean} returns true if major versions are equal + */ + static canMigrate(decoratorCommandSet, DCS_VERSION) { + const inputVersion = ModelUtil.parseNamespace(ModelUtil.getNamespace(decoratorCommandSet.$class)).version; + return (semver.major(inputVersion) === semver.major(DCS_VERSION) && (semver.minor(inputVersion) < semver.minor(DCS_VERSION))); + } + /** * Applies all the decorator commands from the DecoratorCommandSet * to the ModelManager. @@ -157,11 +207,29 @@ class DecoratorManager { * with respect to to decorator command set model * @param {boolean} [options.validateCommands] - validate the decorator command set targets. Note that * the validate option must also be true + * @param {boolean} [options.migrate] - migrate the decoratorCommandSet $class to match the dcs model version * @returns {ModelManager} a new model manager with the decorations applied */ static decorateModels(modelManager, decoratorCommandSet, options) { + + if (options?.migrate && this.canMigrate(decoratorCommandSet, DCS_VERSION)) { + decoratorCommandSet = this.migrateTo(decoratorCommandSet, DCS_VERSION); + } + if (options?.validate) { - const validationModelManager = DecoratorManager.validate(decoratorCommandSet, modelManager.getModelFiles()); + const validationModelManager = new ModelManager({ + strict: true, + metamodelValidation: true, + addMetamodel: true, + }); + validationModelManager.addModelFiles(modelManager.getModelFiles()); + validationModelManager.addCTOModel( + DCS_MODEL, + 'decoratorcommands@0.3.0.cto' + ); + const factory = new Factory(validationModelManager); + const serializer = new Serializer(factory, validationModelManager); + serializer.fromJSON(decoratorCommandSet); if (options?.validateCommands) { decoratorCommandSet.commands.forEach((command) => { DecoratorManager.validateCommand( @@ -267,6 +335,27 @@ class DecoratorManager { } } + + /** + * Applies a new decorator to the Map element + * @private + * @param {string} element the element to apply the decorator to + * @param {string} target the command target + * @param {*} declaration the map declaration + * @param {string} type the command type + * @param {*} newDecorator the decorator to add + */ + static applyDecoratorForMapElement(element, target, declaration, type, newDecorator ) { + const decl = element === 'KEY' ? declaration.key : declaration.value; + if (target.type) { + if (this.falsyOrEqual(target.type, decl.$class)) { + this.applyDecorator(decl, type, newDecorator); + } + } else { + this.applyDecorator(decl, type, newDecorator); + } + } + /** * Compares two arrays. If the first argument is falsy * the function returns true. @@ -326,12 +415,31 @@ class DecoratorManager { */ static executeCommand(namespace, declaration, command) { const { target, decorator, type } = command; - const { name } = ModelUtil.parseNamespace(namespace); - if ( - this.falsyOrEqual(target.namespace, [namespace, name]) && - this.falsyOrEqual(target.declaration, [declaration.name]) - ) { - if (!target.property && !target.type) { + const { name } = ModelUtil.parseNamespace( namespace ); + if (this.falsyOrEqual(target.namespace, [namespace,name]) && + this.falsyOrEqual(target.declaration, [declaration.name])) { + + if (declaration.$class === `${MetaModelNamespace}.MapDeclaration`) { + if (target.mapElement) { + switch(target.mapElement) { + case 'KEY': + case 'VALUE': + this.applyDecoratorForMapElement(target.mapElement, target, declaration, type, decorator); + break; + case 'KEY_VALUE': + this.applyDecoratorForMapElement('KEY', target, declaration, type, decorator); + this.applyDecoratorForMapElement('VALUE', target, declaration, type, decorator); + break; + } + } else if (target.type) { + if (this.falsyOrEqual(target.type, declaration.key.$class)) { + this.applyDecorator(declaration.key, type, decorator); + } + if (this.falsyOrEqual(target.type, declaration.value.$class)) { + this.applyDecorator(declaration.value, type, decorator); + } + } + } else if (!target.property && !target.type) { this.applyDecorator(declaration, type, decorator); } else { // scalars are declarations but do not have properties diff --git a/packages/concerto-core/test/data/decoratorcommands/incompatible_version_dcs.json b/packages/concerto-core/test/data/decoratorcommands/incompatible_version_dcs.json new file mode 100644 index 000000000..5af274713 --- /dev/null +++ b/packages/concerto-core/test/data/decoratorcommands/incompatible_version_dcs.json @@ -0,0 +1,189 @@ +{ + "$class" : "org.accordproject.decoratorcommands@0.2.0.DecoratorCommandSet", + "name" : "web", + "version": "1.0.0", + "commands" : [ + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "APPEND", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType", + "declaration" : "Dictionary", + "mapElement": "KEY" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "TEST", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType", + "mapElement": "KEY" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Foo", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "mapElement": "KEY" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Qux", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "type" : "concerto.metamodel@1.0.0.StringMapValueType", + "mapElement": "VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Bar", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "mapElement": "VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Quux", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "mapElement": "KEY_VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Baz", + "arguments" : [] + } + }, + + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType", + "mapElement": "KEY_VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Bazola", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapValueType", + "mapElement": "KEY_VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Bongo", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "DecoratesKeyByType", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "type" : "concerto.metamodel@1.0.0.StringMapValueType" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "DecoratesValueByType", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "DecoratesAllMapKeys", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.2.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.2.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapValueType" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "DecoratesAllMapValues", + "arguments" : [] + } + } + ] +} diff --git a/packages/concerto-core/test/data/decoratorcommands/map-declaration.json b/packages/concerto-core/test/data/decoratorcommands/map-declaration.json new file mode 100644 index 000000000..92db66dd9 --- /dev/null +++ b/packages/concerto-core/test/data/decoratorcommands/map-declaration.json @@ -0,0 +1,189 @@ +{ + "$class" : "org.accordproject.decoratorcommands@0.3.0.DecoratorCommandSet", + "name" : "web", + "version": "1.0.0", + "commands" : [ + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "APPEND", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType", + "declaration" : "Dictionary", + "mapElement": "KEY" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "TEST", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType", + "mapElement": "KEY" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Foo", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "mapElement": "KEY" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Qux", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "type" : "concerto.metamodel@1.0.0.StringMapValueType", + "mapElement": "VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Bar", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "mapElement": "VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Quux", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "mapElement": "KEY_VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Baz", + "arguments" : [] + } + }, + + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType", + "mapElement": "KEY_VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Bazola", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapValueType", + "mapElement": "KEY_VALUE" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "Bongo", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "DecoratesKeyByType", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "declaration" : "Dictionary", + "type" : "concerto.metamodel@1.0.0.StringMapValueType" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "DecoratesValueByType", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "DecoratesAllMapKeys", + "arguments" : [] + } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test@1.0.0", + "type" : "concerto.metamodel@1.0.0.StringMapValueType" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "DecoratesAllMapValues", + "arguments" : [] + } + } + ] +} diff --git a/packages/concerto-core/test/data/decoratorcommands/test.cto b/packages/concerto-core/test/data/decoratorcommands/test.cto index 1595a6c50..abfd024f5 100644 --- a/packages/concerto-core/test/data/decoratorcommands/test.cto +++ b/packages/concerto-core/test/data/decoratorcommands/test.cto @@ -12,4 +12,15 @@ concept Person { o String address1 o String address2 o Integer zip + o Dictionary dictionary +} + +map Dictionary { + o String + o String +} + +map Rolodex { + o String + o String } \ No newline at end of file diff --git a/packages/concerto-core/test/decoratormanager.js b/packages/concerto-core/test/decoratormanager.js index a635d10cb..99d8a6269 100644 --- a/packages/concerto-core/test/decoratormanager.js +++ b/packages/concerto-core/test/decoratormanager.js @@ -26,6 +26,7 @@ chai.use(require('chai-as-promised')); describe('DecoratorManager', () => { beforeEach(() => { + process.env.ENABLE_MAP_TYPE = 'true'; // TODO Remove on release of MapType }); afterEach(() => { @@ -175,6 +176,159 @@ describe('DecoratorManager', () => { (decoratorZipProperty ===null).should.be.true; }); + it('should decorate the specified element on the specified Map Declaration (Map Key)', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync('./test/data/decoratorcommands/map-declaration.json', 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); + dictionary.should.not.be.null; + dictionary.key.getDecorator('Foo').should.not.be.null; + dictionary.key.getDecorator('Qux').should.not.be.null; + }); + + it('should auto upgrade decoratorcommands $class minor version if it is below DCS_VERSION (asserts decorators are correctly applied)', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync('./test/data/decoratorcommands/incompatible_version_dcs.json', 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true, migrate: true}); + + const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); + dictionary.should.not.be.null; + dictionary.key.getDecorator('Foo').should.not.be.null; + dictionary.key.getDecorator('Qux').should.not.be.null; + }); + + + it('should auto upgrade decoratorcommands $class minor version if it is below DCS_VERSION (asserts correct upgrade on DCS $class properties)', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + let dcs = fs.readFileSync('./test/data/decoratorcommands/incompatible_version_dcs.json', 'utf-8'); + dcs = DecoratorManager.migrateTo(JSON.parse(dcs), '0.3.0'); + + dcs.$class.should.equal('org.accordproject.decoratorcommands@0.3.0.DecoratorCommandSet'); + dcs.commands[0].$class.should.equal('org.accordproject.decoratorcommands@0.3.0.Command'); + dcs.commands[0].target.$class.should.equal('org.accordproject.decoratorcommands@0.3.0.CommandTarget'); + dcs.commands[0].target.type.should.equal('concerto.metamodel@1.0.0.StringMapKeyType'); // concerto metamodel $class does not change + dcs.commands[0].decorator.$class.should.equal('concerto.metamodel@1.0.0.Decorator'); // concerto metamodel $class does not change + }); + + it('should decorate the specified type on the specified Map Declaration (Map Key)', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync('./test/data/decoratorcommands/map-declaration.json', 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); + dictionary.should.not.be.null; + dictionary.key.getDecorator('DecoratesKeyByType').should.not.be.null; + }); + + it('should decorate the specified element on the specified Map Declaration (Map Value)', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync('./test/data/decoratorcommands/map-declaration.json', 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); + + dictionary.should.not.be.null; + dictionary.value.getDecorator('Bar').should.not.be.null; + dictionary.value.getDecorator('Quux').should.not.be.null; + }); + + it('should decorate the specified type on the specified Map Declaration (Map Value)', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync('./test/data/decoratorcommands/map-declaration.json', 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); + dictionary.should.not.be.null; + dictionary.value.getDecorator('DecoratesValueByType').should.not.be.null; + }); + + it('should decorate both Key and Value elements on the specified Map Declaration', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync('./test/data/decoratorcommands/map-declaration.json', 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); + + dictionary.should.not.be.null; + dictionary.key.getDecorator('Baz').should.not.be.null; + dictionary.value.getDecorator('Baz').should.not.be.null; + }); + + it('should decorate a Key and Value element on an unspecified Map Declaration when a type is specified (type takes precedence over element value KEY_VALUE)', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync('./test/data/decoratorcommands/map-declaration.json', 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); + + dictionary.should.not.be.null; + dictionary.key.getDecorator('Bazola').should.not.be.null; + dictionary.value.getDecorator('Bongo').should.not.be.null; + }); + + it('should decorate all Map Declaration Key and Value elements on the model when a declaration is not specified', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true, skipLocationNodes: true}); + const modelText = fs.readFileSync('./test/data/decoratorcommands/test.cto', 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync('./test/data/decoratorcommands/map-declaration.json', 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + + const dictionary = decoratedModelManager.getType('test@1.0.0.Dictionary'); + const rolodex = decoratedModelManager.getType('test@1.0.0.Rolodex'); + + dictionary.should.not.be.null; + dictionary.key.getDecorator('DecoratesAllMapKeys').should.not.be.null; + dictionary.value.getDecorator('DecoratesAllMapValues').should.not.be.null; + + rolodex.should.not.be.null; + rolodex.key.getDecorator('DecoratesAllMapKeys').should.not.be.null; + rolodex.value.getDecorator('DecoratesAllMapValues').should.not.be.null; + }); + it('should fail with invalid command', async function() { // load a model to decorate const testModelManager = new ModelManager({strict:true}); diff --git a/packages/concerto-core/types/lib/decoratormanager.d.ts b/packages/concerto-core/types/lib/decoratormanager.d.ts index 183409d8a..a6b644280 100644 --- a/packages/concerto-core/types/lib/decoratormanager.d.ts +++ b/packages/concerto-core/types/lib/decoratormanager.d.ts @@ -19,6 +19,14 @@ declare class DecoratorManager { * @throws {Error} throws an error if the decoratorCommandSet is invalid */ static validate(decoratorCommandSet: any, modelFiles?: ModelFile[]): ModelManager; + /** + * Rewrites the $class property on decoratorCommandSet classes. + * @private + * @param {*} decoratorCommandSet the DecoratorCommandSet object + * @param {string} version the DCS version upgrade target + * @returns {object} the migrated DecoratorCommandSet object + */ + private static migrateTo; /** * Applies all the decorator commands from the DecoratorCommandSet * to the ModelManager. @@ -29,11 +37,13 @@ declare class DecoratorManager { * with respect to to decorator command set model * @param {boolean} [options.validateCommands] - validate the decorator command set targets. Note that * the validate option must also be true + * @param {boolean} [options.migrate] - migrate the decoratorCommandSet $class to match the dcs model version * @returns {ModelManager} a new model manager with the decorations applied */ static decorateModels(modelManager: ModelManager, decoratorCommandSet: any, options?: { validate?: boolean; validateCommands?: boolean; + migrate?: boolean; }): ModelManager; /** * Throws an error if the decoractor command is invalid @@ -41,6 +51,16 @@ declare class DecoratorManager { * @param {*} command the decorator command */ static validateCommand(validationModelManager: ModelManager, command: any): void; + /** + * Applies a new decorator to the Map element + * @private + * @param {string} element the element to apply the decorator to + * @param {string} target the command target + * @param {*} declaration the map declaration + * @param {string} type the command type + * @param {*} newDecorator the decorator to add + */ + private static applyDecoratorForMapElement; /** * Compares two arrays. If the first argument is falsy * the function returns true.