From 02dfcdb44eaed848f01b24e53cb3c66dbcae6eae Mon Sep 17 00:00:00 2001 From: Jonathan Casey <jonathan.casey@docusign.com> Date: Thu, 19 Oct 2023 15:37:56 +0100 Subject: [PATCH 1/3] feat(map): handle imports for map key Signed-off-by: Jonathan Casey <jonathan.casey@docusign.com> --- .../lib/introspect/mapkeytype.js | 3 +- .../lib/introspect/mapvaluetype.js | 19 +---------- packages/concerto-core/lib/modelutil.js | 32 ++++++++++++++++--- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index 74e79d876..acf7ef280 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -47,6 +47,7 @@ class MapKeyType extends Decorated { constructor(parent, ast) { super(ast); this.parent = parent; + this.modelFile = parent.getModelFile(); this.process(); } @@ -70,7 +71,7 @@ class MapKeyType extends Decorated { validate() { if (!ModelUtil.isPrimitiveType(this.type)) { - let decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type?.name); + const decl = ModelUtil.getTypeDeclaration(this.ast.type.name, this.modelFile); if (!ModelUtil.isValidMapKeyScalar(decl)) { throw new IllegalModelException( diff --git a/packages/concerto-core/lib/introspect/mapvaluetype.js b/packages/concerto-core/lib/introspect/mapvaluetype.js index c80391ee6..9ede733a0 100644 --- a/packages/concerto-core/lib/introspect/mapvaluetype.js +++ b/packages/concerto-core/lib/introspect/mapvaluetype.js @@ -69,7 +69,7 @@ class MapValueType extends Decorated { */ validate() { if (!ModelUtil.isPrimitiveType(this.type)) { - const decl = this.getTypeDeclaration(this.ast.type.name); + const decl = ModelUtil.getTypeDeclaration(this.ast.type.name, this.modelFile); // All declarations, with the exception of MapDeclarations, are valid Values. if(decl.isMapDeclaration?.()) { @@ -184,23 +184,6 @@ class MapValueType extends Decorated { isValue() { return true; } - - /** - * Returns the corresponding ClassDeclaration representation of the Type - * - * @param {string} type - the Type of the Map Value - * @return {Object} the corresponding ClassDeclaration representation - * @private - */ - getTypeDeclaration(type) { - if (this.modelFile.isLocalType(this.ast.type.name)) { - return this.modelFile.getAllDeclarations().find(d => d.name === this.ast.type.name); - } else { - const fqn = this.modelFile.resolveImport(this.ast.type.name); - return this.modelFile.getModelManager().getType(fqn); - } - } - } module.exports = MapValueType; diff --git a/packages/concerto-core/lib/modelutil.js b/packages/concerto-core/lib/modelutil.js index 502badb58..44a269d0b 100644 --- a/packages/concerto-core/lib/modelutil.js +++ b/packages/concerto-core/lib/modelutil.js @@ -19,6 +19,13 @@ const { MetaModelUtil } = require('@accordproject/concerto-metamodel'); const semver = require('semver'); const Globalize = require('./globalize'); +// Types needed for TypeScript generation. +/* eslint-disable no-unused-vars */ +/* istanbul ignore next */ +if (global === undefined) { + const ModelFile = require('../lib/introspect/modelfile'); +} + const ID_REGEX = /^(\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|\p{Nl}|\$|_|\\u[0-9A-Fa-f]{4})(?:\p{Lu}|\p{Ll}|\p{Lt}|\p{Lm}|\p{Lo}|\p{Nl}|\$|_|\\u[0-9A-Fa-f]{4}|\p{Mn}|\p{Mc}|\p{Nd}|\p{Pc}|\u200C|\u200D)*$/u; const privateReservedProperties = [ @@ -315,11 +322,11 @@ class ModelUtil { } /** - * Returns true if this Value is a valid Map Value. - * - * @param {Object} value - the Value of the Map Declaration - * @return {boolean} true if the Value is a valid Map Value - */ + * Returns true if this Value is a valid Map Value. + * + * @param {Object} value - the Value of the Map Declaration + * @return {boolean} true if the Value is a valid Map Value + */ static isValidMapValue(value) { return [ `${MetaModelNamespace}.BooleanMapValueType`, @@ -331,6 +338,21 @@ class ModelUtil { `${MetaModelNamespace}.ObjectMapValueType` ].includes(value.$class); } + + /** + * Returns the corresponding ClassDeclaration representation of the Map Type + * @param {string} type - the Type of the Map Value + * @param {ModelFile} modelFile - the ModelFile that owns the Property + * @return {Object} the corresponding ClassDeclaration representation + */ + static getTypeDeclaration(type, modelFile) { + if (modelFile.isLocalType(type)) { + return modelFile.getAllDeclarations().find(d => d.name === type); + } else { + const fqn = modelFile.resolveImport(type); + return modelFile.getModelManager().getType(fqn); + } + } } module.exports = ModelUtil; From 72baa6b9b94a70800ba9cf8b7009ccd4ff2a5e2f Mon Sep 17 00:00:00 2001 From: Jonathan Casey <jonathan.casey@docusign.com> Date: Thu, 19 Oct 2023 15:41:24 +0100 Subject: [PATCH 2/3] feat(map): add test coverage Signed-off-by: Jonathan Casey <jonathan.casey@docusign.com> --- .../test/data/parser/mapdeclaration/base.cto | 20 ++++++++ .../mapdeclaration.badkey.imported.thing.cto | 21 ++++++++ ...mapdeclaration.goodkey.imported.scalar.cto | 21 ++++++++ ...apdeclaration.goodvalue.imported.thing.cto | 21 ++++++++ .../test/introspect/mapdeclaration.js | 50 ++++++++++++++----- 5 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 packages/concerto-core/test/data/parser/mapdeclaration/base.cto create mode 100644 packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.imported.thing.cto create mode 100644 packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.imported.scalar.cto create mode 100644 packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.imported.thing.cto diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/base.cto b/packages/concerto-core/test/data/parser/mapdeclaration/base.cto new file mode 100644 index 000000000..867f10ee5 --- /dev/null +++ b/packages/concerto-core/test/data/parser/mapdeclaration/base.cto @@ -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. + */ +namespace com.testing.base@1.0.0 + +concept Thing { + o String name +} + +scalar Time extends DateTime diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.imported.thing.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.imported.thing.cto new file mode 100644 index 000000000..a3882d7f8 --- /dev/null +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.imported.thing.cto @@ -0,0 +1,21 @@ +/* + * 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. + */ +namespace com.testing@1.0.0 + +import com.testing.base@1.0.0.{Thing} + +map Dictionary { + o Thing + o String +} \ No newline at end of file diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.imported.scalar.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.imported.scalar.cto new file mode 100644 index 000000000..cec56933a --- /dev/null +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.imported.scalar.cto @@ -0,0 +1,21 @@ +/* + * 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. + */ +namespace com.testing@1.0.0 + +import com.testing.base@1.0.0.Time + +map Dictionary { + o Time + o String +} \ No newline at end of file diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.imported.thing.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.imported.thing.cto new file mode 100644 index 000000000..3cbf54e8b --- /dev/null +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.imported.thing.cto @@ -0,0 +1,21 @@ +/* + * 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. + */ +namespace com.testing@1.0.0 + +import com.testing.base@1.0.0.Thing + +map Dictionary { + o String + o Thing +} \ No newline at end of file diff --git a/packages/concerto-core/test/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index 641abd06c..c5e753f35 100644 --- a/packages/concerto-core/test/introspect/mapdeclaration.js +++ b/packages/concerto-core/test/introspect/mapdeclaration.js @@ -25,6 +25,7 @@ const ParserUtil = require('./parserutility'); const ModelManager = require('../../lib/modelmanager'); const Util = require('../composer/composermodelutility'); +const fs = require('fs'); const sinon = require('sinon'); const expect = require('chai').expect; @@ -46,7 +47,7 @@ describe('MapDeclaration', () => { describe('#constructor', () => { - it('should throw if ast contains no Map Key Property', () => { + it('should throw if ast contains no Map Key Type', () => { (() => { new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', @@ -162,6 +163,13 @@ describe('MapDeclaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.scalar.string.cto', MapDeclaration); decl.validate(); }); + + it('should validate when map key is imported and is of valid map key type', () => { + const base_cto = fs.readFileSync('test/data/parser/mapdeclaration/base.cto', 'utf-8'); + introspectUtils.modelManager.addCTOModel(base_cto, 'base.cto'); + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.imported.scalar.cto', MapDeclaration); + decl.validate(); + }); }); describe('#validate success scenarios - Map Value', () => { @@ -254,11 +262,18 @@ describe('MapDeclaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.long.cto', MapDeclaration); decl.validate(); }); + + it('should validate when map value is imported and is of valid map key type', () => { + const base_cto = fs.readFileSync('test/data/parser/mapdeclaration/base.cto', 'utf-8'); + introspectUtils.modelManager.addCTOModel(base_cto, 'base.cto'); + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.imported.thing.cto', MapDeclaration); + decl.validate(); + }); }); describe('#validate failure scenarios - Map Key', () => { - it('should throw if ast contains illegal Map Key Property', () => { + it('should throw if ast contains illegal Map Key Type', () => { (() => { new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', @@ -273,42 +288,42 @@ describe('MapDeclaration', () => { }).should.throw(IllegalModelException); }); - it('should throw if ast contains illegal Map Key Property - Concept', () => { + it('should throw if ast contains illegal Map Key Type - Concept', () => { (() => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.concept.cto', MapDeclaration); decl.validate().should.throw(IllegalModelException); }); }); - it('should throw if ast contains illegal Map Key Property - Scalar Long', () => { + it('should throw if ast contains illegal Map Key Type - Scalar Long', () => { (() => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.scalar.long.cto', MapDeclaration); decl.validate(); }); }); - it('should throw if ast contains illegal Map Key Property - Scalar Integer', () => { + it('should throw if ast contains illegal Map Key Type - Scalar Integer', () => { (() => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.scalar.integer.cto', MapDeclaration); decl.validate().should.throw(IllegalModelException); }); }); - it('should throw if ast contains illegal Map Key Property - Scalar Double', () => { + it('should throw if ast contains illegal Map Key Type - Scalar Double', () => { (() => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.scalar.double.cto', MapDeclaration); decl.validate().should.throw(IllegalModelException); }); }); - it('should throw if ast contains illegal Map Key Property - Scalar Boolean', () => { + it('should throw if ast contains illegal Map Key Type - Scalar Boolean', () => { (() => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.scalar.boolean.cto', MapDeclaration); decl.validate().should.throw(IllegalModelException); }); }); - it('should throw if ast contains illegal Map Key Property - Boolean', () => { + it('should throw if ast contains illegal Map Key Type - Boolean', () => { (() => { new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', @@ -323,7 +338,7 @@ describe('MapDeclaration', () => { }).should.throw(IllegalModelException); }); - it('should throw if ast contains illegal Map Key Property - Integer', () => { + it('should throw if ast contains illegal Map Key Type - Integer', () => { (() => { new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', @@ -338,7 +353,7 @@ describe('MapDeclaration', () => { }).should.throw(IllegalModelException); }); - it('should throw if ast contains illegal Map Key Property - Long', () => { + it('should throw if ast contains illegal Map Key Type - Long', () => { (() => { new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', @@ -353,7 +368,7 @@ describe('MapDeclaration', () => { }).should.throw(IllegalModelException); }); - it('should throw if ast contains illegal Map Key Property - Double', () => { + it('should throw if ast contains illegal Map Key Type - Double', () => { (() => { new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', @@ -368,7 +383,7 @@ describe('MapDeclaration', () => { }).should.throw(IllegalModelException); }); - it('should throw if ast contains illegal Map Key Property - Enum', () => { + it('should throw if ast contains illegal Map Key Type - Enum', () => { (() => { new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', @@ -386,6 +401,15 @@ describe('MapDeclaration', () => { }); }).should.throw(IllegalModelException); }); + + it('should throw when map key is imported and is an illegal Map Key Type', () => { + (() => { + const base_cto = fs.readFileSync('test/data/parser/mapdeclaration/base.cto', 'utf-8'); + introspectUtils.modelManager.addCTOModel(base_cto, 'base.cto'); + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.imported.thing.cto', MapDeclaration); + decl.validate().should.throw(IllegalModelException); + }); + }); }); describe('#validate failure scenarios - Map Value', () => { @@ -489,7 +513,7 @@ describe('MapDeclaration', () => { }); describe('#getKey', () => { - it('should return the map key property', () => { + it('should return the Map Key Type', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', From 85097092bbf350195dcbf189ba714435eee1c42e Mon Sep 17 00:00:00 2001 From: Jonathan Casey <jonathan.casey@docusign.com> Date: Thu, 19 Oct 2023 15:41:43 +0100 Subject: [PATCH 3/3] feat(map): add type defs Signed-off-by: Jonathan Casey <jonathan.casey@docusign.com> --- .../types/lib/decoratormanager.d.ts | 9 +++++++++ .../types/lib/introspect/mapkeytype.d.ts | 2 ++ .../types/lib/introspect/mapvaluetype.d.ts | 8 -------- packages/concerto-core/types/lib/modelutil.d.ts | 17 ++++++++++++----- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/concerto-core/types/lib/decoratormanager.d.ts b/packages/concerto-core/types/lib/decoratormanager.d.ts index a6b644280..4fa7ce2f1 100644 --- a/packages/concerto-core/types/lib/decoratormanager.d.ts +++ b/packages/concerto-core/types/lib/decoratormanager.d.ts @@ -27,6 +27,15 @@ declare class DecoratorManager { * @returns {object} the migrated DecoratorCommandSet object */ private static migrateTo; + /** + * 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 + */ + private static canMigrate; /** * Applies all the decorator commands from the DecoratorCommandSet * to the ModelManager. diff --git a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts index e1461945f..7599252b2 100644 --- a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts @@ -17,6 +17,7 @@ declare class MapKeyType extends Decorated { */ constructor(parent: MapDeclaration, ast: any); parent: MapDeclaration; + modelFile: ModelFile; /** * Semantic validation of the structure of this class. * @@ -60,3 +61,4 @@ declare class MapKeyType extends Decorated { } import Decorated = require("./decorated"); import MapDeclaration = require("./mapdeclaration"); +import ModelFile = require("./modelfile"); diff --git a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts index 70818d901..d2bce3b76 100644 --- a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts @@ -57,14 +57,6 @@ declare class MapValueType extends Decorated { * @return {boolean} true if the class is a Map Value */ isValue(): boolean; - /** - * Returns the corresponding ClassDeclaration representation of the Type - * - * @param {string} type - the Type of the Map Value - * @return {Object} the corresponding ClassDeclaration representation - * @private - */ - private getTypeDeclaration; } import Decorated = require("./decorated"); import MapDeclaration = require("./mapdeclaration"); diff --git a/packages/concerto-core/types/lib/modelutil.d.ts b/packages/concerto-core/types/lib/modelutil.d.ts index 473d2c255..62515449c 100644 --- a/packages/concerto-core/types/lib/modelutil.d.ts +++ b/packages/concerto-core/types/lib/modelutil.d.ts @@ -156,10 +156,17 @@ declare class ModelUtil { */ static isValidMapKeyScalar(decl: any): boolean; /** - * Returns true if this Value is a valid Map Value. - * - * @param {Object} value - the Value of the Map Declaration - * @return {boolean} true if the Value is a valid Map Value - */ + * Returns true if this Value is a valid Map Value. + * + * @param {Object} value - the Value of the Map Declaration + * @return {boolean} true if the Value is a valid Map Value + */ static isValidMapValue(value: any): boolean; + /** + * Returns the corresponding ClassDeclaration representation of the Map Type + * @param {string} type - the Type of the Map Value + * @param {Object} modelFile - the ModelFile that owns the Property + * @return {Object} the corresponding ClassDeclaration representation + */ + static getTypeDeclaration(type: string, modelFile: any): any; }