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;
 }