diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 139c8c287..1576e1a7a 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -59,13 +59,16 @@ class DecoratorExtractor { } class DecoratorManager { + ModelManager validate(decoratorCommandSet,ModelFile[]) throws Error - + ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?,boolean?) + + ModelManager decorateModels(ModelManager,decoratorCommandSet,object?,boolean?,boolean?,boolean?,boolean?) + ExtractDecoratorsResult extractDecorators(ModelManager,object,boolean,string) + void validateCommand(ModelManager,command) + Boolean falsyOrEqual(string||,string[]) + void applyDecorator(decorated,string,newDecorator) - + void executeCommand(string,declaration,command) + + void executeNamespaceCommand(model,command) + + void executeCommand(string,declaration,command,boolean?) + void executePropertyCommand(property,command) + + void checkForNamespaceTargetAndApplyDecorator(declaration,string,decorator,target,boolean?) + + Boolean checkForNamespaceTarget(boolean?) } + string[] intersect() + boolean isUnversionedNamespaceEqual() diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index d5e1b93ff..4b53e2cc6 100644 --- a/packages/concerto-core/changelog.txt +++ b/packages/concerto-core/changelog.txt @@ -25,6 +25,9 @@ # +Version 3.16.8 {e3fd3aa83c0d4767f64efeb36640c9e0} 2024-07-09 +- Added a new pathway for applying dcs target at namespace + Version 3.16.7 {8f455df1e788c4994f423d6e236bee21} 2024-05-01 - Added missing `strictQualifiedDateTimes` option to Serializer.fromJSON diff --git a/packages/concerto-core/index.js b/packages/concerto-core/index.js index b8387461e..c5a3df149 100644 --- a/packages/concerto-core/index.js +++ b/packages/concerto-core/index.js @@ -102,6 +102,9 @@ const Concerto = require('./lib/concerto'); // MetaModel const MetaModel = require('./lib/introspect/metamodel'); +// ConcertoCodes +const ConcertoCodes = require('./lib/concertoCodes'); + // Version /** @type {{ name: string, version: string }} */ const version = require('./package.json'); @@ -148,5 +151,6 @@ module.exports = { DateTimeUtil, Concerto, MetaModel, + ConcertoCodes, version }; diff --git a/packages/concerto-core/lib/concertoCodes.js b/packages/concerto-core/lib/concertoCodes.js new file mode 100644 index 000000000..d7780032e --- /dev/null +++ b/packages/concerto-core/lib/concertoCodes.js @@ -0,0 +1,20 @@ +/* + * 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'; + +//deprecation codes +const CONCERTO_DEPRECATION_001 = 'concerto-dep:001'; + +module.exports = { CONCERTO_DEPRECATION_001 }; diff --git a/packages/concerto-core/lib/decoratormanager.js b/packages/concerto-core/lib/decoratormanager.js index fc7860093..ed83c6a19 100644 --- a/packages/concerto-core/lib/decoratormanager.js +++ b/packages/concerto-core/lib/decoratormanager.js @@ -21,6 +21,8 @@ const ModelUtil = require('./modelutil'); const { MetaModelNamespace } = require('@accordproject/concerto-metamodel'); const semver = require('semver'); const DecoratorExtractor = require('./decoratorextractor'); +const { Warning, ErrorCodes } = require('@accordproject/concerto-util'); +const ConcertoCodes = require('./concertoCodes'); // Types needed for TypeScript generation. /* eslint-disable no-unused-vars */ @@ -209,6 +211,7 @@ class DecoratorManager { * @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 + * @param {boolean} [options.enableDcsNamespaceTarget] - flag to control applying namespace targeted decorators on top of the namespace instead of all declarations in that namespace * @returns {ModelManager} a new model manager with the decorations applied */ static decorateModels(modelManager, decoratorCommandSet, options) { @@ -245,7 +248,10 @@ class DecoratorManager { decoratedAst.models.forEach((model) => { model.declarations.forEach((decl) => { decoratorCommandSet.commands.forEach((command) => { - this.executeCommand(model.namespace, decl, command); + this.executeCommand(model.namespace, decl, command, options?.enableDcsNamespaceTarget); + if(this.checkForNamespaceTarget(options?.enableDcsNamespaceTarget)) { + this.executeNamespaceCommand(model, command); + } }); }); }); @@ -440,15 +446,35 @@ class DecoratorManager { } } + /** + * Executes a Command against a Model Namespace, adding + * decorators to the Namespace. + * @param {*} model the model + * @param {*} command the Command object from the dcs + * org.accordproject.decoratorcommands model + */ + static executeNamespaceCommand(model, command) { + const { target, decorator, type } = command; + + if (Object.keys(target).length === 2 && target.namespace) { + const { name } = ModelUtil.parseNamespace( model.namespace ); + // should we just compare with namespace?? + if(this.falsyOrEqual(target.namespace, [model.namespace,name])) { + this.applyDecorator(model, type, decorator); + } + } + } + /** * Executes a Command against a ClassDeclaration, adding * decorators to the ClassDeclaration, or its properties, as required. * @param {string} namespace the namespace for the declaration * @param {*} declaration the class declaration - * @param {*} command the Command object from the + * @param {*} command the Command object from the dcs + * @param {boolean} [enableDcsNamespaceTarget] - flag to control applying namespace targeted decorators on top of the namespace instead of all declarations in that namespace * org.accordproject.decoratorcommands model */ - static executeCommand(namespace, declaration, command) { + static executeCommand(namespace, declaration, command, enableDcsNamespaceTarget) { const { target, decorator, type } = command; const { name } = ModelUtil.parseNamespace( namespace ); if (this.falsyOrEqual(target.namespace, [namespace,name]) && @@ -474,10 +500,10 @@ class DecoratorManager { this.applyDecorator(declaration.value, type, decorator); } } else { - this.applyDecorator(declaration, type, decorator); + this.checkForNamespaceTargetAndApplyDecorator(declaration, type, decorator, target, enableDcsNamespaceTarget); } } else if (!(target.property || target.properties || target.type)) { - this.applyDecorator(declaration, type, decorator); + this.checkForNamespaceTargetAndApplyDecorator(declaration, type, decorator, target, enableDcsNamespaceTarget); } else { // scalars are declarations but do not have properties if (declaration.properties) { @@ -511,6 +537,46 @@ class DecoratorManager { this.applyDecorator(property, type, decorator); } } + + /** + * Checks if enableDcsNamespaceTarget or ENABLE_DCS_TARGET_NAMESPACE is enabled or not + * if enabled, applies the decorator on top of the namespace or else on all declarations + * within the namespace. + * @param {*} declaration the type to apply the decorator to + * @param {string} type the command type + * @param {*} decorator the decorator to add + * @param {*} target the target object for the decorator + * @param {boolean} [enableDcsNamespaceTarget] - flag to control applying namespace targeted decorators on top of the namespace instead of all declarations in that namespace + */ + static checkForNamespaceTargetAndApplyDecorator(declaration, type, decorator, target, enableDcsNamespaceTarget) { + if(this.checkForNamespaceTarget(enableDcsNamespaceTarget)) { + if (target.declaration) { + this.applyDecorator(declaration, type, decorator); + } + } else { + this.applyDecorator(declaration, type, decorator); + } + } + + /** + * Checks if enableDcsNamespaceTarget or ENABLE_DCS_TARGET_NAMESPACE is enabled or not + * and print depreaction warning if not enabled and return boolean value as well + * @param {boolean} [enableDcsNamespaceTarget] - flag to control applying namespace targeted decorators on top of the namespace instead of all declarations in that namespace + * @returns {Boolean} true if either of the flags is enabled + */ + static checkForNamespaceTarget(enableDcsNamespaceTarget) { + if(enableDcsNamespaceTarget || process.env.ENABLE_DCS_NAMESPACE_TARGET === 'true') { + return true; + } else { + Warning.printDeprecationWarning( + 'Functionality for namespace targeted Decorator Command Sets has beed changed. Using namespace targets to apply decorators on all declarations in a namespace will be deprecated soon.', + ErrorCodes.DEPRECATION_WARNING, + ConcertoCodes.CONCERTO_DEPRECATION_001, + 'Please refer See https://concerto.accordproject.org/depreaction#001' + ); + return false; + } + } } module.exports = DecoratorManager; diff --git a/packages/concerto-core/lib/introspect/modelfile.js b/packages/concerto-core/lib/introspect/modelfile.js index e95d31ad4..bb9922a94 100644 --- a/packages/concerto-core/lib/introspect/modelfile.js +++ b/packages/concerto-core/lib/introspect/modelfile.js @@ -746,6 +746,7 @@ class ModelFile extends Decorated { if (this.getModelManager().isStrict()){ throw new Error('Wilcard Imports are not permitted in strict mode.'); } + // Should we change this as well? console.warn('DEPRECATED: Wilcard Imports are deprecated in this version of Concerto and will be removed in a future version.'); this.importWildcardNamespaces.push(imp.namespace); break; diff --git a/packages/concerto-core/test/data/decoratorcommands/web.json b/packages/concerto-core/test/data/decoratorcommands/web.json index 96ee26837..00a4303d0 100644 --- a/packages/concerto-core/test/data/decoratorcommands/web.json +++ b/packages/concerto-core/test/data/decoratorcommands/web.json @@ -153,6 +153,19 @@ "name" : "Address", "arguments" : [] } + }, + { + "$class" : "org.accordproject.decoratorcommands@0.3.0.Command", + "type" : "UPSERT", + "target" : { + "$class" : "org.accordproject.decoratorcommands@0.3.0.CommandTarget", + "namespace" : "test" + }, + "decorator" : { + "$class" : "concerto.metamodel@1.0.0.Decorator", + "name" : "IsValid", + "arguments" : [] + } } ] } diff --git a/packages/concerto-core/test/decoratormanager.js b/packages/concerto-core/test/decoratormanager.js index 20481c3f1..ebde57a59 100644 --- a/packages/concerto-core/test/decoratormanager.js +++ b/packages/concerto-core/test/decoratormanager.js @@ -22,6 +22,8 @@ const VocabularyManager= require('../../concerto-vocabulary/lib/vocabularymanage const Printer= require('../../concerto-cto/lib/printer'); const chai = require('chai'); +const { DEPRECATION_WARNING } = require('@accordproject/concerto-util/lib/errorcodes'); +const { CONCERTO_DEPRECATION_001 } = require('../lib/concertoCodes'); require('chai').should(); chai.use(require('chai-things')); chai.use(require('chai-as-promised')); @@ -121,7 +123,7 @@ describe('DecoratorManager', () => { decoratedModelManager.should.not.be.null; }); - it('should add decorator', async function() { + it('should add decorators that target declarations', async function() { // load a model to decorate const testModelManager = new ModelManager({strict:true}); const modelText = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/test.cto'), 'utf-8'); @@ -138,10 +140,105 @@ describe('DecoratorManager', () => { const decl = decoratedModelManager.getType('test@1.0.0.Person'); decl.should.not.be.null; decl.getDecorator('Editable').should.not.be.null; + }); + + /* + This test is target to the functionality wherein if there exists a namespace targeted decorator, it applies the decorator to + all the declarations within the namespace, which has been identified as bug and will be deprecated. + */ + it('should add decorators that target namespace and catch warning - behaviour to be deprecated', async function() { + // event listner to catch the warning + process.once('warning', (warning) => { + chai.expect(warning.message).to.be.equals('Functionality for namespace targeted Decorator Command Sets has beed changed. Using namespace targets to apply decorators on all declarations in a namespace will be deprecated soon.'); + chai.expect(warning.name).to.be.equals(DEPRECATION_WARNING); + chai.expect(warning.code).to.be.equals(CONCERTO_DEPRECATION_001); + chai.expect(warning.detail).to.be.equals('Please refer See https://concerto.accordproject.org/depreaction#001'); + }); + // load a model to decorate + const testModelManager = new ModelManager({strict:true}); + const modelText = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/test.cto'), 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/web.json'), 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const modelFile = decoratedModelManager.getModelFile('test@1.0.0'); + modelFile.should.not.be.null; + chai.expect(modelFile.getDecorator('IsValid')).to.be.null; + + const ssnDecl = decoratedModelManager.getType('test@1.0.0.SSN'); + ssnDecl.should.not.be.null; + ssnDecl.getDecorator('IsValid').should.not.be.null; + + const decl = decoratedModelManager.getType('test@1.0.0.Person'); + decl.should.not.be.null; + decl.getDecorator('IsValid').should.not.be.null; + }); + + /* + This test is target to the functionality wherein if there exists a namespace targeted decorator, it applies the decorator to the + namespace, which is the new feature added can be accessed using the feature flag: ENABLE_DCS_NAMESPACE_TARGET. + */ + it('should add decorators that target namespace - updated behaviour using environment variable', async function() { + process.env.ENABLE_DCS_NAMESPACE_TARGET = 'true'; + // load a model to decorate + const testModelManager = new ModelManager({strict:true}); + const modelText = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/test.cto'), 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/web.json'), 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const modelFile = decoratedModelManager.getModelFile('test@1.0.0'); + modelFile.should.not.be.null; + modelFile.getDecorator('IsValid').should.not.be.null; + + const ssnDecl = decoratedModelManager.getType('test@1.0.0.SSN'); + ssnDecl.should.not.be.null; + chai.expect(ssnDecl.getDecorator('IsValid')).to.be.null; + process.env.ENABLE_DCS_NAMESPACE_TARGET = 'false'; + }); + + /* + This test is target to the functionality wherein if there exists a namespace targeted decorator, it applies the decorator to the + namespace, which is the new feature added can be accessed using the option parameter: enableDcsNamespaceTarget. + */ + it('should add decorators that target namespace - updated behaviour using options parameter', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true}); + const modelText = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/test.cto'), 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/web.json'), 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true, enableDcsNamespaceTarget: true}); + + const modelFile = decoratedModelManager.getModelFile('test@1.0.0'); + modelFile.should.not.be.null; + modelFile.getDecorator('IsValid').should.not.be.null; + + const ssnDecl = decoratedModelManager.getType('test@1.0.0.SSN'); + ssnDecl.should.not.be.null; + chai.expect(ssnDecl.getDecorator('IsValid')).to.be.null; + }); + + it('should add decorators that target properties', async function() { + // load a model to decorate + const testModelManager = new ModelManager({strict:true}); + const modelText = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/test.cto'), 'utf-8'); + testModelManager.addCTOModel(modelText, 'test.cto'); + + const dcs = fs.readFileSync(path.join(__dirname,'/data/decoratorcommands/web.json'), 'utf-8'); + const decoratedModelManager = DecoratorManager.decorateModels( testModelManager, JSON.parse(dcs), + {validate: true, validateCommands: true}); + + const decl = decoratedModelManager.getType('test@1.0.0.Person'); + decl.should.not.be.null; const firstNameProperty = decl.getProperty('firstName'); firstNameProperty.should.not.be.null; - const decoratorFormFirstName = firstNameProperty.getDecorator('Form'); decoratorFormFirstName.should.not.be.null; decoratorFormFirstName.getArguments()[0].should.equal('inputType'); @@ -567,4 +664,4 @@ describe('DecoratorManager', () => { }); }); -}); \ No newline at end of file +}); diff --git a/packages/concerto-util/index.js b/packages/concerto-util/index.js index f1d3611dc..36ea61380 100644 --- a/packages/concerto-util/index.js +++ b/packages/concerto-util/index.js @@ -58,6 +58,9 @@ const ErrorCodes = require('./lib/errorcodes'); // NullUtil const NullUtil = require('./lib/null'); +// Warning +const Warning = require('./lib/warning'); + module.exports = { BaseException, BaseFileException, @@ -75,5 +78,6 @@ module.exports = { Label, Identifiers, ErrorCodes, - NullUtil + NullUtil, + Warning }; diff --git a/packages/concerto-util/lib/errorcodes.js b/packages/concerto-util/lib/errorcodes.js index 3f6ec0628..06e299631 100644 --- a/packages/concerto-util/lib/errorcodes.js +++ b/packages/concerto-util/lib/errorcodes.js @@ -22,5 +22,7 @@ const DEFAULT_VALIDATOR_EXCEPTION = 'DefaultValidatorException'; const REGEX_VALIDATOR_EXCEPTION = 'RegexValidatorException'; // base exception for Type not found const TYPE_NOT_FOUND_EXCEPTION = 'TypeNotFoundException'; +// deprecation warning type for process.emitWarning +const DEPRECATION_WARNING = 'DeprecationWarning'; -module.exports = {DEFAULT_BASE_EXCEPTION, DEFAULT_VALIDATOR_EXCEPTION, REGEX_VALIDATOR_EXCEPTION, TYPE_NOT_FOUND_EXCEPTION}; \ No newline at end of file +module.exports = {DEFAULT_BASE_EXCEPTION, DEFAULT_VALIDATOR_EXCEPTION, REGEX_VALIDATOR_EXCEPTION, TYPE_NOT_FOUND_EXCEPTION, DEPRECATION_WARNING}; diff --git a/packages/concerto-util/lib/warning.js b/packages/concerto-util/lib/warning.js new file mode 100644 index 000000000..5bce5bbef --- /dev/null +++ b/packages/concerto-util/lib/warning.js @@ -0,0 +1,40 @@ +/* + * 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'; + +let isWarningEmitted = false; + +/** + * Emits DeprecationWaring to stderr only once and can be caught using an warning event listener as well, please define the code + * and document the deprecation code on https://concerto.accordproject.org/depreaction + * @param {string} message - message of the deprecation warning + * @param {string} type - type of the deprecation warning + * @param {string} code - code of the deprecation warning + * @param {string} detail - detail of the deprecation warning + */ +function printDeprecationWarning(message, type, code, detail) { + if (!isWarningEmitted) { + isWarningEmitted = true; + process.emitWarning(message, { + type, + code, + detail + }); + } +} + +module.exports = { + printDeprecationWarning +};