From 202983335b39bce305986e81c82d845dfd676cfa Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 10:16:08 +0100 Subject: [PATCH 01/41] feat(map): add to introspection API Signed-off-by: jonathan.casey --- .../concerto-core/lib/introspect/mapkeytype.js | 18 ++++++++++++++++++ .../lib/introspect/mapvaluetype.js | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index e8a6743da..bc1fecfdd 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -129,6 +129,24 @@ class MapKeyType extends Decorated { toString() { return 'MapKeyType {id=' + this.getType() + '}'; } + + /** + * Returns true if this class is the definition of a Map Key. + * + * @return {boolean} true if the class is a Map Key + */ + isKey() { + return true; + } + + /** + * Returns true if this class is the definition of a Map Value. + * + * @return {boolean} true if the class is a Map Value + */ + isValue() { + return false; + } } module.exports = MapKeyType; diff --git a/packages/concerto-core/lib/introspect/mapvaluetype.js b/packages/concerto-core/lib/introspect/mapvaluetype.js index 36dc7af00..7359c1d85 100644 --- a/packages/concerto-core/lib/introspect/mapvaluetype.js +++ b/packages/concerto-core/lib/introspect/mapvaluetype.js @@ -121,6 +121,24 @@ class MapValueType extends Decorated { return 'MapValueType {id=' + this.getType() + '}'; } + /** + * Returns true if this class is the definition of a Map Key. + * + * @return {boolean} true if the class is a Map Key + */ + isKey() { + return false; + } + + /** + * Returns true if this class is the definition of a Map Value. + * + * @return {boolean} true if the class is a Map Value + */ + isValue() { + return true; + } + } module.exports = MapValueType; From 1539a647753f476f080d95708180eb78bbaca1d6 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 10:18:20 +0100 Subject: [PATCH 02/41] feat(map): extend JsonGenerator for Map Types Signed-off-by: jonathan.casey --- .../lib/serializer/jsongenerator.js | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/concerto-core/lib/serializer/jsongenerator.js b/packages/concerto-core/lib/serializer/jsongenerator.js index 2cef9574a..0445cbf78 100644 --- a/packages/concerto-core/lib/serializer/jsongenerator.js +++ b/packages/concerto-core/lib/serializer/jsongenerator.js @@ -85,7 +85,40 @@ class JSONGenerator { */ visitMapDeclaration(mapDeclaration, parameters) { const obj = parameters.stack.pop(); - return Object.fromEntries(obj); + + // initialise Map with $class property + let map = new Map([['$class',obj.get('$class')]]); + + obj.forEach((value, key) => { + + if (typeof key === 'object') { + let decl = mapDeclaration.getModelFile() + .getAllDeclarations() + .find(decl => decl.name === key.getType()); + + // convert declaration to JSON representation + parameters.stack.push(key); + const jsonKey = decl.accept(this, parameters); + + key = JSON.stringify(jsonKey); + } + + if (typeof value === 'object') { + let decl = mapDeclaration.getModelFile() + .getAllDeclarations() + .find(decl => decl.name === value.getType()); + + // convert declaration to JSON representation + parameters.stack.push(value); + const jsonValue = decl.accept(this, parameters); + + value = JSON.stringify(jsonValue); + } + + map.set(key, value); + }); + + return Object.fromEntries(map); } /** From fcd0b2010b7d7fa1b39e0b96471a62f6428d9095 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 10:23:04 +0100 Subject: [PATCH 03/41] feat(map): extend JsonPopulator for Map Types Signed-off-by: jonathan.casey --- .../lib/serializer/jsonpopulator.js | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index 33f89fb39..fc846d05d 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -170,13 +170,60 @@ class JSONPopulator { * @private */ visitMapDeclaration(mapDeclaration, parameters) { - const jsonObj = parameters.jsonStack.pop(); + let jsonObj = parameters.jsonStack.pop(); parameters.path ?? (parameters.path = new TypedStack('$')); - // throws if Map contains private reserved properties as keys. + // Throws if Map contains reserved properties - a Map containing reserved Properties should not be serialized. getAssignableProperties(jsonObj, mapDeclaration); - return new Map(Object.entries(jsonObj)); + jsonObj = new Map(Object.entries(jsonObj)); + + let map = new Map(); + + jsonObj.forEach((value, key) => { + + if (key === '$class') { + map.set(key, value); + return; + } + + // If its a Non-Primitive, its likely a ClassDeclaration which needs visiting. + if (!ModelUtil.isSystemProperty(key) && !ModelUtil.isPrimitiveType(key)) { + // get the Key thing. + let thing = mapDeclaration.getModelFile() + .getAllDeclarations() + .find(decl => decl.name === mapDeclaration.getKey().getType()); + + // parse the Object, and visit the declaration. + if (thing?.isClassDeclaration()) { + let subResource = parameters.factory.newConcept(thing.getNamespace(), + thing.getName(), thing.getIdentifierFieldName() ); + parameters.jsonStack.push(JSON.parse(key)); + parameters.resourceStack.push(subResource); + key = thing.accept(this, parameters); + } + } + + if (!ModelUtil.isPrimitiveType(mapDeclaration.getValue().getType())) { + // get the Value thing. + let thing = mapDeclaration.getModelFile() + .getAllDeclarations() + .find(decl => decl.name === mapDeclaration.getValue().getType()); + + // parse the Object, and visit the declaration. + if (thing?.isClassDeclaration() ) { + let subResource = parameters.factory.newConcept(thing.getNamespace(), + thing.getName(), thing.getIdentifierFieldName() ); + parameters.jsonStack.push(JSON.parse(value)); + parameters.resourceStack.push(subResource); + value = thing.accept(this, parameters); + } + } + + map.set(key, value); + }); + + return map; } /** From a3f247a7d26d916e15e872918f5c2bb925b35eb8 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 10:28:01 +0100 Subject: [PATCH 04/41] feat(map): extend ResourceValidator for Map Types Signed-off-by: jonathan.casey --- .../lib/serializer/resourcevalidator.js | 88 ++++++++++++++++--- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/packages/concerto-core/lib/serializer/resourcevalidator.js b/packages/concerto-core/lib/serializer/resourcevalidator.js index d303d3294..1c72ec370 100644 --- a/packages/concerto-core/lib/serializer/resourcevalidator.js +++ b/packages/concerto-core/lib/serializer/resourcevalidator.js @@ -22,6 +22,10 @@ const ModelUtil = require('../modelutil'); const ValidationException = require('./validationexception'); const Globalize = require('../globalize'); +const dayjs = require('dayjs'); +const utc = require('dayjs/plugin/utc'); +dayjs.extend(utc); + /** *

* Validates a Resource or Field against the models defined in the ModelManager. @@ -106,21 +110,84 @@ class ResourceValidator { return null; } + + /** + * Check a Type that is declared as a Map Type. + * @param {Object} type - the type in scope for validation, can be MapTypeKey or MapTypeValue + * @param {Object} value - the object being validated + * @param {Object} parameters - the parameter + * @param {Map} mapDeclaration - the object being visited + * @private + */ + checkMapType(type, value, parameters, mapDeclaration, ) { + + if (!ModelUtil.isPrimitiveType(type.getType())) { + + // thing might be a Concept, Scalar String, Scalar DateTime + let thing = mapDeclaration.getModelFile() + .getAllDeclarations() + .find(decl => decl.name === type.getType()); + + // a ClassDeclaration used in the context of a Map Key must be identified. + if (type.isKey() && thing.isClassDeclaration() && !thing.isIdentified()) { + throw new Error('Map Key must be an Identified Class Declaration'); + } + + // if Key or Value is Scalar, get the Base Type of the Scalar for primitive validation. + if (ModelUtil.isScalar(mapDeclaration.getKey())) { + type = thing.getType(); + } + + if (thing.isClassDeclaration()) { + parameters.stack.push(value); + thing.accept(this, parameters); + return; + } + } else { + // otherwise its a primitive + type = type.getType(); + + } + + // validate the primitive + switch(type) { + case 'String': + if (typeof value !== 'string') { + throw new Error(`Model violation in ${mapDeclaration.getFullyQualifiedName()}. Expected Type of String but found '${value}' instead.`); + } + break; + case 'DateTime': + if (!dayjs.utc(value).isValid()) { + throw new Error(`Model violation in ${mapDeclaration.getFullyQualifiedName()}. Expected Type of DateTime but found '${value}' instead.`); + } + break; + case 'Boolean': + if (typeof value !== 'boolean') { + const type = typeof value; + throw new Error(`Model violation in ${mapDeclaration.getFullyQualifiedName()}. Expected Type of Boolean but found ${type} instead, for value '${value}'.`); + } + break; + } + } + /** * Visitor design pattern * * @param {MapDeclaration} mapDeclaration - the object being visited * @param {Object} parameters - the parameter + * @return {Object} the result of visiting or null + * * @private */ visitMapDeclaration(mapDeclaration, parameters) { + const obj = parameters.stack.pop(); if (!((obj instanceof Map))) { throw new Error('Expected a Map, but found ' + JSON.stringify(obj)); } - if(!obj.has('$class')) { + if (!obj.has('$class')) { throw new Error('Invalid Map. Map must contain a properly formatted $class property'); } @@ -128,17 +195,14 @@ class ResourceValidator { throw new Error(`$class value must match ${mapDeclaration.getFullyQualifiedName()}`); } - obj.forEach((value, key) => { - if(!ModelUtil.isSystemProperty(key)) { - if (typeof key !== 'string') { - ResourceValidator.reportInvalidMap(parameters.rootResourceIdentifier, mapDeclaration, obj); - } - if (typeof value !== 'string') { - ResourceValidator.reportInvalidMap(parameters.rootResourceIdentifier, mapDeclaration, obj); - } + if (!ModelUtil.isSystemProperty(key)) { + this.checkMapType(mapDeclaration.getKey(), key, parameters, mapDeclaration); + this.checkMapType(mapDeclaration.getValue(), value, parameters, mapDeclaration); } }); + + return null; } /** @@ -505,14 +569,16 @@ class ResourceValidator { * @param {string} id - the identifier of this instance. * @param {MapDeclaration} mapDeclaration - the declaration of the map * @param {Object} value - the value of the field. + * @param {Object} type - the type of the field. * @private */ - static reportInvalidMap(id, mapDeclaration, value) { + static reportInvalidMap(id, mapDeclaration, value, type) { let formatter = Globalize.messageFormatter('resourcevalidator-invalidmap'); throw new ValidationException(formatter({ resourceId: id, classFQN: mapDeclaration.getFullyQualifiedName(), - invalidValue: value.toString() + invalidValue: value.toString(), + typeOfValue: type })); } From 48172667e782383e5cad2403dd0e2340b1da149e Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 10:37:03 +0100 Subject: [PATCH 05/41] feat(map): add test cases for Map serialisation <> deserialisation Signed-off-by: jonathan.casey --- .../test/serializer/maptype/serializer.js | 897 ++++++++++++++++++ 1 file changed, 897 insertions(+) create mode 100644 packages/concerto-core/test/serializer/maptype/serializer.js diff --git a/packages/concerto-core/test/serializer/maptype/serializer.js b/packages/concerto-core/test/serializer/maptype/serializer.js new file mode 100644 index 000000000..a0e836d1a --- /dev/null +++ b/packages/concerto-core/test/serializer/maptype/serializer.js @@ -0,0 +1,897 @@ +/* + * 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'; + +const Factory = require('../../../lib/factory'); +const ModelManager = require('../../../lib/modelmanager'); +const Resource = require('../../../lib/model/resource'); +const Serializer = require('../../../lib/serializer'); +const Util = require('../../composer/composermodelutility'); +const ModelUtil = require('../../../../concerto-core/lib/modelutil'); + +const should = require('chai').should(); +const sinon = require('sinon'); + +describe('Serializer', () => { + + let sandbox; + let factory; + let modelManager; + let serializer; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + modelManager = new ModelManager(); + Util.addComposerModel(modelManager); + modelManager.addCTOModel(` + namespace org.acme.sample + + + concept Concepts { + o Dictionary dict optional + o Diary diary optional + o Timer timer optional + o StopWatch stopwatch optional + o RSVP rsvp optional + o Database database optional + o Appointment appointment optional + o MarriageRegister marriages optional + o PostCodeEntries postcodes optional + o AddressBook addressBook optional + o Birthday birthday optional + o Celebration celebration optional + o ExaminationGrade grade optional + o Rolodex rolodex optional + o Attendance attendance optional + o Vegan vegan optional + o Reservation reservation optional + o GuestList vip optional + o Graduated graduated optional + o Directory directory optional + o Meeting meeting optional + o Graduation graduation optional + o Team team optional + } + + map Dictionary { + o String + o String + } + + map PhoneBook { + o String + o String + } + + map Diary { + o DateTime + o String + } + + map Timer { + o DateTime + o DateTime + } + + map StopWatch { + o Time + o Time + } + + map RSVP { + o String + o Boolean + } + + map Attendance { + o DateTime + o Boolean + } + + map Meeting { + o Time + o Person + } + + map Database { + o GUID + o String + } + + map Vegan { + o GUID + o Boolean + } + + map Directory { + o GUID + o Person + } + + map Appointment { + o Time + o String + } + + map Birthday { + o String + o DateTime + } + + map Celebration { + o String + o Time + } + + map Graduation { + o DateTime + o Person + } + + map Rolodex { + o String + o Person + } + + map MarriageRegister { + o Person + o Person + } + + map ExaminationGrade { + o Person + o String + } + + map Reservation { + o Person + o Time + } + + map Graduated { + o Person + o DateTime + } + + map GuestList { + o Person + o Boolean + } + + map PostCodeEntries { + o GUID + o PostalCode + } + + map AddressBook { + o GUID + --> Person + } + + map Team { + o Person + o Leader + } + + scalar GUID extends String + + scalar Time extends DateTime + + scalar PostalCode extends String + + concept Person identified by name { + o String name + } + + concept Leader { + o String name + } + `); + factory = new Factory(modelManager); + serializer = new Serializer(factory, modelManager); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('#toJSON', () => { + + it('should generate a JSON object with a Map ', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.dict = new Map(); + concept.dict.set('$class', 'org.acme.sample.Dictionary'); + concept.dict.set('Lorem', 'Ipsum'); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + dict: { + $class: 'org.acme.sample.Dictionary', + Lorem: 'Ipsum' + } + }); + }); + + it('should generate a JSON object with a Map ', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.rsvp = new Map(); + concept.rsvp.set('$class', 'org.acme.sample.RSVP'); + concept.rsvp.set('Lorem', true); + concept.rsvp.set('Ipsum', false); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + rsvp: { + $class: 'org.acme.sample.RSVP', + 'Lorem': true, + 'Ipsum': false, + } + }); + }); + + it('should generate a JSON object with a Map ', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.birthday = new Map(); + concept.birthday.set('$class', 'org.acme.sample.Birthday'); + concept.birthday.set('Ipsum', '2023-10-28T01:02:03Z'); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + birthday: { + $class: 'org.acme.sample.Birthday', + 'Ipsum': '2023-10-28T01:02:03Z' + } + }); + }); + + it('should generate a JSON object with a Map ', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + + concept.rolodex = new Map(); + concept.rolodex.set('$class', 'org.acme.sample.Rolodex'); + concept.rolodex.set('Abbeyleix', person); + + const json = serializer.toJSON(concept); + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + rolodex: { + $class: 'org.acme.sample.Rolodex', + 'Abbeyleix': '{"$class":"org.acme.sample.Person","name":"Bob"}' + } + }); + }); + + it('should generate a JSON object with a Map , where Scalar extends String', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.celebration = new Map(); + concept.celebration.set('$class', 'org.acme.sample.Celebration'); + concept.celebration.set('D4F45017-AD2B-416B-AD9F-3B74F7DEA291', '2022-11-28T01:02:03Z'); + concept.celebration.set('9FAE34BF-18C3-4770-A6AA-6F7656C356B8', '2023-10-28T01:02:03Z'); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + celebration: { + $class: 'org.acme.sample.Celebration', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': '2022-11-28T01:02:03Z', + '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': '2023-10-28T01:02:03Z', + } + }); + }); + + it('should generate a JSON object with a Map ', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.diary = new Map(); + concept.diary.set('$class', 'org.acme.sample.Diary'); + concept.diary.set('2023-10-28T01:02:03Z', 'Ipsum'); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + diary: { + $class: 'org.acme.sample.Diary', + '2023-10-28T01:02:03Z': 'Ipsum', + } + }); + }); + + it('should generate a JSON object with a Map ', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.attendance = new Map(); + concept.attendance.set('$class', 'org.acme.sample.Attendance'); + concept.attendance.set('2023-10-28T01:02:03Z', true); + concept.attendance.set('2023-11-28T01:02:03Z', false); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + attendance: { + $class: 'org.acme.sample.Attendance', + '2023-10-28T01:02:03Z': true, + '2023-11-28T01:02:03Z': false, + } + }); + }); + + it('should generate a JSON object with a Map ', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + + concept.graduation = new Map(); + concept.graduation.set('$class', 'org.acme.sample.Graduation'); + concept.graduation.set('2023-10-28T01:02:03Z', person); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + graduation: { + $class: 'org.acme.sample.Graduation', + '2023-10-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Bob"}' + } + }); + }); + + it('should generate a JSON object with a Map , where Scalar extends String', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.stopwatch = new Map(); + concept.stopwatch.set('$class', 'org.acme.sample.StopWatch'); + concept.stopwatch.set('2023-10-28T00:02:03Z', '2023-10-28T01:02:03Z'); + concept.stopwatch.set('2023-11-28T00:02:03Z', '2023-11-28T01:02:03Z'); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + stopwatch: { + $class: 'org.acme.sample.StopWatch', + '2023-10-28T00:02:03Z': '2023-10-28T01:02:03Z', + '2023-11-28T00:02:03Z': '2023-11-28T01:02:03Z', + } + }); + }); + + it('should generate a JSON object with a Map , where Scalar extends String', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.database = new Map(); + concept.database.set('$class', 'org.acme.sample.Database'); + concept.database.set('D4F45017-AD2B-416B-AD9F-3B74F7DEA291', 'Lorem'); + concept.database.set('9FAE34BF-18C3-4770-A6AA-6F7656C356B8', 'Ipsum'); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + database: { + $class: 'org.acme.sample.Database', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': 'Lorem', + '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': 'Ipsum', + } + }); + }); + + it('should generate a JSON object with a Map , where Scalar extends String', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.vegan = new Map(); + concept.vegan.set('$class', 'org.acme.sample.Vegan'); + concept.vegan.set('D4F45017-AD2B-416B-AD9F-3B74F7DEA291', true); + concept.vegan.set('9FAE34BF-18C3-4770-A6AA-6F7656C356B8', false); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + vegan: { + $class: 'org.acme.sample.Vegan', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': true, + '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': false, + } + }); + }); + + it('should generate a JSON object with a Map , where Scalar extends String', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); + + concept.directory = new Map(); + concept.directory.set('$class', 'org.acme.sample.Directory'); + concept.directory.set('D4F45017-AD2B-416B-AD9F-3B74F7DEA291', bob); + concept.directory.set('9FAE34BF-18C3-4770-A6AA-6F7656C356B8', alice); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + directory: { + $class: 'org.acme.sample.Directory', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': '{"$class":"org.acme.sample.Person","name":"Bob"}', + '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': '{"$class":"org.acme.sample.Person","name":"Alice"}', + } + }); + }); + + it('should generate a JSON object with a Map , where Scalar extends DateTime', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.appointment = new Map(); + concept.appointment.set('$class', 'org.acme.sample.Appointment'); + concept.appointment.set('2023-10-28T01:02:03Z', 'Lorem'); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + appointment: { + $class: 'org.acme.sample.Appointment', + '2023-10-28T01:02:03Z': 'Lorem', + } + }); + }); + + it('should generate a JSON object with a Map , where Scalar extends DateTime', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + + concept.meeting = new Map(); + concept.meeting.set('$class', 'org.acme.sample.Meeting'); + concept.meeting.set('2023-10-28T01:02:03Z', person); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + meeting: { + $class: 'org.acme.sample.Meeting', + '2023-10-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Bob"}' + } + }); + }); + + it('should generate a JSON object with a Map , where Concept is Identified', () => { + let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + + const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); + + concepts.grade = new Map(); + concepts.grade.set('$class', 'org.acme.sample.ExaminationGrade'); + concepts.grade.set(bob, 'A+'); + concepts.grade.set(alice, 'B+'); + + const json = serializer.toJSON(concepts); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + grade: { + '$class': 'org.acme.sample.ExaminationGrade', + '{"$class":"org.acme.sample.Person","name":"Bob"}' : 'A+', + '{"$class":"org.acme.sample.Person","name":"Alice"}' : 'B+' + } + }); + }); + + it('should generate a JSON object with a Map , where Concept is Identified & Scalar extends DateTime ', () => { + let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + + const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + + concepts.reservation = new Map(); + concepts.reservation.set('$class', 'org.acme.sample.Reservation'); + concepts.reservation.set(person, '2023-10-28T01:02:03Z'); + + const json = serializer.toJSON(concepts); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + reservation: { + $class: 'org.acme.sample.Reservation', + '{"$class":"org.acme.sample.Person","name":"Bob"}' : '2023-10-28T01:02:03Z' + } + }); + }); + + it('should generate a JSON object with a Map , where Concept is Identified', () => { + let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + + const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + + concepts.vip = new Map(); + concepts.vip.set('$class', 'org.acme.sample.GuestList'); + concepts.vip.set(person, true); + + const json = serializer.toJSON(concepts); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + vip: { + $class: 'org.acme.sample.GuestList', + '{"$class":"org.acme.sample.Person","name":"Bob"}' : true + } + }); + }); + + it('should generate a JSON object with a Map , where Concept is Identified', () => { + let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + + const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); + + concepts.marriages = new Map(); + concepts.marriages.set('$class', 'org.acme.sample.MarriageRegister'); + concepts.marriages.set(bob, alice); + + const json = serializer.toJSON(concepts); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + marriages: { + $class: 'org.acme.sample.MarriageRegister', + '{"$class":"org.acme.sample.Person","name":"Bob"}' : '{"$class":"org.acme.sample.Person","name":"Alice"}' + } + }); + }); + + it('should generate a JSON object with a Map , where Concept is Identified', () => { + let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + + const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + + concepts.graduated = new Map(); + concepts.graduated.set('$class', 'org.acme.sample.Graduated'); + concepts.graduated.set(person, '2023-10-28T01:02:03Z'); + + const json = serializer.toJSON(concepts); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + graduated: { + $class: 'org.acme.sample.Graduated', + '{"$class":"org.acme.sample.Person","name":"Bob"}' : '2023-10-28T01:02:03Z' + } + }); + }); + + it('should throw if bad Key value is provided for Map, where Key Type DateTime is expected', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.appointment = new Map(); + concept.appointment.set('$class', 'org.acme.sample.Appointment'); + concept.appointment.set('BAD-DATE-28T01:02:03Z', 'Lorem'); // Bad DateTime + + (() => { + serializer.toJSON(concept); + }).should.throw('Model violation in org.acme.sample.Appointment. Expected Type of DateTime but found \'BAD-DATE-28T01:02:03Z\' instead.'); + }); + + it('should throw if bad Key value is provided for Map, where Key Type String is expected', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.dict = new Map(); + concept.dict.set('$class', 'org.acme.sample.Dictionary'); + concept.dict.set(1234, 'Lorem'); // Bad key + + (() => { + serializer.toJSON(concept); + }).should.throw('Model violation in org.acme.sample.Dictionary. Expected Type of String but found \'1234\' instead.'); + }); + + it('should throw if a bad Value is Supplied for Map, where Value type Boolean is expected', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.rsvp = new Map(); + concept.rsvp.set('$class', 'org.acme.sample.RSVP'); + concept.rsvp.set('Lorem', true); + concept.rsvp.set('Ipsum', 'false'); + + (() => { + serializer.toJSON(concept); + }).should.throw('Model violation in org.acme.sample.RSVP. Expected Type of Boolean but found string instead, for value \'false\'.'); + }); + + it('should throw if a bad Value is Supplied for Map, where Value type String is expected', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.dict = new Map(); + concept.dict.set('$class', 'org.acme.sample.Dictionary'); + concept.dict.set('Lorem', 1234); + + (() => { + serializer.toJSON(concept); + }).should.throw('Model violation in org.acme.sample.Dictionary. Expected Type of String but found \'1234\' instead.'); + }); + + + it('should throw if a bad value is Supplied for Map - where Value type Boolean is expected', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.timer = new Map(); + concept.timer.set('$class', 'org.acme.sample.Timer'); + concept.timer.set('2023-10-28T01:02:03Z', '2023-10-28T01:02:03Z'); + concept.timer.set('2023-10-28T01:02:03Z', 'BAD-DATE-VALUE'); + + (() => { + serializer.toJSON(concept); + }).should.throw('Model violation in org.acme.sample.Timer. Expected Type of DateTime but found \'BAD-DATE-VALUE\' instead.'); + }); + + it('should throw if the value of a Map is not a Map instance', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.dict = 'xyz'; // bad value + + (() => { + serializer.toJSON(concept); + }).should.throw(`Expected a Map, but found ${JSON.stringify(concept.dict)}`); + }); + + it('should throw validation error when there is a mismatch on map $class property', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.dict = new Map(); + concept.dict.set('$class', 'org.acme.sample.PhoneBook'); // dict is not a PhoneBook. + concept.dict.set('Lorem', 'Ipsum'); + + (() => { + serializer.toJSON(concept); + }).should.throw('$class value must match org.acme.sample.Dictionary'); + }); + }); + + describe('#fromJSON', () => { + + it('should deserialize a JSON object with a Map ', () => { + let json = { + $class: 'org.acme.sample.Concepts', + dict: { + '$class': 'org.acme.sample.Dictionary', + 'Lorem': 'Ipsum' + } + }; + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.dict.should.be.an.instanceOf(Map); + resource.dict.get('$class').should.equal('org.acme.sample.Dictionary'); + resource.dict.get('Lorem').should.equal('Ipsum'); + }); + + it('should deserialize a JSON object with a Map , where Scalar extends String', () => { + let json = { + $class: 'org.acme.sample.Concepts', + database: { + '$class': 'org.acme.sample.Database', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': 'Ipsum' + } + }; + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.database.should.be.an.instanceOf(Map); + resource.database.get('$class').should.equal('org.acme.sample.Database'); + resource.database.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').should.equal('Ipsum'); + }); + + it('should deserialize a JSON object with a Map , where Scalar extends DateTime', () => { + let json = { + $class: 'org.acme.sample.Concepts', + appointment: { + '$class': 'org.acme.sample.Appointment', + '2023-10-28T01:02:03Z': 'Ipsum' + } + }; + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.appointment.should.be.an.instanceOf(Map); + resource.appointment.get('$class').should.equal('org.acme.sample.Appointment'); + resource.appointment.get('2023-10-28T01:02:03Z').should.equal('Ipsum'); + }); + + it('should deserialize a JSON object with a Map , where both Key & Value are Identified Concepts', () => { + let json = { + $class: 'org.acme.sample.Concepts', + marriages: { + '$class': 'org.acme.sample.MarriageRegister', + '{"$class":"org.acme.sample.Person","name":"Bob"}': '{"$class":"org.acme.sample.Person","name":"Alice"}' + } + }; + + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.marriages.should.be.an.instanceOf(Map); + resource.marriages.get('$class', 'org.acme.sample.MarriageRegister'); + resource.marriages.forEach((value, key) => { + if (!ModelUtil.isSystemProperty(key)) { + key.should.be.an.instanceOf(Resource); + key.name.should.equal('Bob'); + value.should.be.an.instanceOf(Resource); + value.name.should.equal('Alice'); + } + }); + }); + + it('should deserialize a JSON object with a Map ', () => { + let json = { + $class: 'org.acme.sample.Concepts', + grade: { + '$class': 'org.acme.sample.ExaminationGrade', + '{"$class":"org.acme.sample.Person","name":"Bob"}': 'A+' + } + }; + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.grade.should.be.an.instanceOf(Map); + resource.grade.get('$class', 'org.acme.sample.ExaminationGrade'); + resource.grade.forEach((value, key) => { + if (!ModelUtil.isSystemProperty(key)) { + key.should.be.an.instanceOf(Resource); + key.name.should.equal('Bob'); + value.should.be.a('String'); + value.should.equal('A+'); + } + }); + }); + + it('should deserialize a JSON object with a Map ', () => { + let json = { + $class: 'org.acme.sample.Concepts', + graduated: { + '$class': 'org.acme.sample.Graduated', + '{"$class":"org.acme.sample.Person","name":"Bob"}': '2023-10-28T01:02:03Z' + } + }; + + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.graduated.should.be.an.instanceOf(Map); + resource.graduated.get('$class', 'org.acme.sample.Graduated'); + resource.graduated.forEach((value, key) => { + if (!ModelUtil.isSystemProperty(key)) { + key.should.be.an.instanceOf(Resource); + key.name.should.equal('Bob'); + value.should.be.a('String'); + value.should.equal('2023-10-28T01:02:03Z'); + } + }); + }); + + it('should deserialize a JSON object with a Map ', () => { + let json = { + $class: 'org.acme.sample.Concepts', + reservation: { + '$class': 'org.acme.sample.Reservation', + '{"$class":"org.acme.sample.Person","name":"Bob"}': '2023-10-28T01:02:03Z' + } + }; + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.reservation.should.be.an.instanceOf(Map); + resource.reservation.get('$class', 'org.acme.sample.Reservation'); + resource.reservation.forEach((value, key) => { + if (!ModelUtil.isSystemProperty(key)) { + key.should.be.an.instanceOf(Resource); + key.name.should.equal('Bob'); + value.should.be.a('String'); + value.should.equal('2023-10-28T01:02:03Z'); + } + }); + }); + + it('should deserialize a JSON object with a Map ', () => { + let json = { + $class: 'org.acme.sample.Concepts', + vip: { + '$class': 'org.acme.sample.GuestList', + '{"$class":"org.acme.sample.Person","name":"Bob"}': true + } + }; + + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.vip.should.be.an.instanceOf(Map); + resource.vip.get('$class', 'org.acme.sample.GuestList'); + resource.vip.forEach((value, key) => { + if (!ModelUtil.isSystemProperty(key)) { + key.should.be.an.instanceOf(Resource); + key.name.should.equal('Bob'); + value.should.be.a('Boolean'); + value.should.equal(true); + } + }); + }); + + it('should deserialize a JSON object with a Map , where Value is a Non-Identified Concept', () => { + let json = { + $class: 'org.acme.sample.Concepts', + team: { + '$class': 'org.acme.sample.Team', + '{"$class":"org.acme.sample.Person","name":"Bob"}': '{"$class":"org.acme.sample.Person","name":"Alice"}' + } + }; + + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.team.should.be.an.instanceOf(Map); + resource.team.get('$class', 'org.acme.sample.Team'); + resource.team.forEach((value, key) => { + if (!ModelUtil.isSystemProperty(key)) { + key.should.be.an.instanceOf(Resource); + key.name.should.equal('Bob'); + value.should.be.an.instanceOf(Resource); + value.name.should.equal('Alice'); + } + }); + }); + + it('should throw an error when deserializing a Map without a $class property', () => { + let json = { + $class: 'org.acme.sample.Concepts', + dict: { + // '$class': 'org.acme.sample.Dictionary', + 'Lorem': 'Ipsum' + } + }; + (() => { + serializer.fromJSON(json); + }).should.throw('Invalid Map. Map must contain a properly formatted $class property'); + }); + + it('should throw an error when deserializing a Map using a reserved Identifier as a Key property', () => { + let json = { + $class: 'org.acme.sample.Concepts', + dict: { + '$class': 'org.acme.sample.Dictionary', + '$namespace': 'com.reserved.property', + 'Lorem': 'Ipsum' + } + }; + (() => { + serializer.fromJSON(json); + }).should.throw('Unexpected reserved properties for type org.acme.sample.Dictionary: $namespace'); + }); + }); +}); + From d0802b17967371a9fd5c907524543d985ea882ff Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 10:40:12 +0100 Subject: [PATCH 06/41] feat(map): cleanup Signed-off-by: jonathan.casey --- .../lib/serializer/resourcevalidator.js | 18 ------------------ ...declaration.goodkey.declaration.concept.cto | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/concerto-core/lib/serializer/resourcevalidator.js b/packages/concerto-core/lib/serializer/resourcevalidator.js index 1c72ec370..28372714a 100644 --- a/packages/concerto-core/lib/serializer/resourcevalidator.js +++ b/packages/concerto-core/lib/serializer/resourcevalidator.js @@ -564,24 +564,6 @@ class ResourceValidator { })); } - /** - * Throw a new error for a model violation. - * @param {string} id - the identifier of this instance. - * @param {MapDeclaration} mapDeclaration - the declaration of the map - * @param {Object} value - the value of the field. - * @param {Object} type - the type of the field. - * @private - */ - static reportInvalidMap(id, mapDeclaration, value, type) { - let formatter = Globalize.messageFormatter('resourcevalidator-invalidmap'); - throw new ValidationException(formatter({ - resourceId: id, - classFQN: mapDeclaration.getFullyQualifiedName(), - invalidValue: value.toString(), - typeOfValue: type - })); - } - /** * Throw a new error for a model violation. * @param {string} id - the identifier of this instance. diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto index 7ad76e688..9ef43072d 100644 --- a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto @@ -12,7 +12,7 @@ * limitations under the License. */ -namespace com.testing@1.0.0 //todo makek sure namesapce is same for all test concepts +namespace com.testing@1.0.0 concept Person identified {} From 916069dbcaf3be63a2954371fec719d4c79002ff Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 12:29:22 +0100 Subject: [PATCH 07/41] feat(map): add type definitions Signed-off-by: jonathan.casey --- .../types/lib/introspect/mapkeytype.d.ts | 12 ++++++++++++ .../types/lib/introspect/mapvaluetype.d.ts | 12 ++++++++++++ .../lib/serializer/resourcevalidator.d.ts | 19 +++++++++++-------- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts index 600b3605d..3f6c47f19 100644 --- a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts @@ -38,6 +38,18 @@ declare class MapKeyType extends Decorated { * @return {string} the short name of this class */ getType(): string; + /** + * Returns true if this class is the definition of a Map Key. + * + * @return {boolean} true if the class is a Map Key + */ + isKey(): boolean; + /** + * Returns true if this class is the definition of a Map Value. + * + * @return {boolean} true if the class is a Map Value + */ + isValue(): boolean; } import Decorated = require("./decorated"); import MapDeclaration = require("./mapdeclaration"); diff --git a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts index 0713c5bc7..750f109f3 100644 --- a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts @@ -37,6 +37,18 @@ declare class MapValueType extends Decorated { * @return {string} the short name of this class */ getType(): string; + /** + * Returns true if this class is the definition of a Map Key. + * + * @return {boolean} true if the class is a Map Key + */ + isKey(): boolean; + /** + * Returns true if this class is the definition of a Map Value. + * + * @return {boolean} true if the class is a Map Value + */ + isValue(): boolean; } import Decorated = require("./decorated"); import MapDeclaration = require("./mapdeclaration"); diff --git a/packages/concerto-core/types/lib/serializer/resourcevalidator.d.ts b/packages/concerto-core/types/lib/serializer/resourcevalidator.d.ts index b5c2f5854..6f156205e 100644 --- a/packages/concerto-core/types/lib/serializer/resourcevalidator.d.ts +++ b/packages/concerto-core/types/lib/serializer/resourcevalidator.d.ts @@ -36,14 +36,6 @@ declare class ResourceValidator { * @private */ private static reportNotResouceViolation; - /** - * Throw a new error for a model violation. - * @param {string} id - the identifier of this instance. - * @param {MapDeclaration} mapDeclaration - the declaration of the map - * @param {Object} value - the value of the field. - * @private - */ - private static reportInvalidMap; /** * Throw a new error for a model violation. * @param {string} id - the identifier of this instance. @@ -133,11 +125,22 @@ declare class ResourceValidator { * @private */ private visitEnumDeclaration; + /** + * Check a Type that is declared as a Map Type. + * @param {Object} type - the type in scope for validation, can be MapTypeKey or MapTypeValue + * @param {Object} value - the object being validated + * @param {Object} parameters - the parameter + * @param {Map} mapDeclaration - the object being visited + * @private + */ + private checkMapType; /** * Visitor design pattern * * @param {MapDeclaration} mapDeclaration - the object being visited * @param {Object} parameters - the parameter + * @return {Object} the result of visiting or null + * * @private */ private visitMapDeclaration; From 6c239861fedf5213cffa170f25b5f70b65e2c702 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 13:10:04 +0100 Subject: [PATCH 08/41] feat(map): satisfy version checker Signed-off-by: jonathan.casey --- packages/concerto-core/api.txt | 4 ++++ packages/concerto-core/changelog.txt | 3 +++ 2 files changed, 7 insertions(+) diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index b4f35ef91..decb297ac 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -174,6 +174,8 @@ class MapKeyType extends Decorated { + MapDeclaration getParent() + string getType() + String toString() + + boolean isKey() + + boolean isValue() } class MapValueType extends Decorated { + void constructor(MapDeclaration,Object) throws IllegalModelException @@ -182,6 +184,8 @@ class MapValueType extends Decorated { + MapDeclaration getParent() + string getType() + String toString() + + boolean isKey() + + boolean isValue() } + ModelManager newMetaModelManager() + object validateMetaModel() diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index e2fb3352c..a8f18280b 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.8.0 {916069dbcaf3be63a2954371fec719d4c79002ff} 2023-08-1 +- Add To MapKeyType, MapValue functionality + Version 3.7.0 {a97cb6ebd45679354ba4da1940d2bb8d} 2023-05-19 - Add MapDeclaration, MapKeyType, AggregateValueType From fb3c393b9c2f2f032445cc9370deae92a3d8c735 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 13:12:42 +0100 Subject: [PATCH 09/41] feat(map): remove variable assignment for chai import Signed-off-by: jonathan.casey --- packages/concerto-core/test/serializer/maptype/serializer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/concerto-core/test/serializer/maptype/serializer.js b/packages/concerto-core/test/serializer/maptype/serializer.js index a0e836d1a..ec8f5b272 100644 --- a/packages/concerto-core/test/serializer/maptype/serializer.js +++ b/packages/concerto-core/test/serializer/maptype/serializer.js @@ -21,7 +21,7 @@ const Serializer = require('../../../lib/serializer'); const Util = require('../../composer/composermodelutility'); const ModelUtil = require('../../../../concerto-core/lib/modelutil'); -const should = require('chai').should(); +require('chai').should(); const sinon = require('sinon'); describe('Serializer', () => { From a788e448c573c6b6a362986b7ddc98196b86a5b3 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 13:16:47 +0100 Subject: [PATCH 10/41] feat(map): satisfy version checker Signed-off-by: jonathan.casey --- packages/concerto-core/changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index a8f18280b..14994e61f 100644 --- a/packages/concerto-core/changelog.txt +++ b/packages/concerto-core/changelog.txt @@ -24,7 +24,7 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # -Version 3.8.0 {916069dbcaf3be63a2954371fec719d4c79002ff} 2023-08-1 +Version 3.8.1 {794268f69b81f05f711d38a9ef1a7833} 2023-08-1 - Add To MapKeyType, MapValue functionality Version 3.7.0 {a97cb6ebd45679354ba4da1940d2bb8d} 2023-05-19 From 34fbf743607515c7526cd40a409f227cce2a99bf Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 13:43:17 +0100 Subject: [PATCH 11/41] feat(map): update test cases to match on error handling changes Signed-off-by: jonathan.casey --- packages/concerto-core/test/serializer/resourcevalidator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/concerto-core/test/serializer/resourcevalidator.js b/packages/concerto-core/test/serializer/resourcevalidator.js index cb34d35aa..8f4c8cc9f 100644 --- a/packages/concerto-core/test/serializer/resourcevalidator.js +++ b/packages/concerto-core/test/serializer/resourcevalidator.js @@ -363,7 +363,7 @@ describe('ResourceValidator', function () { (() => { mapDeclaration.accept(resourceValidator,parameters ); - }).should.throw('Model violation in the "TEST" instance. Invalid Type for Map Key or Value - expected String type.'); + }).should.throw('Model violation in org.acme.map.PhoneBook. Expected Type of String but found \'3\' instead.'); }); it('should not validate map with bad key', function () { @@ -374,7 +374,7 @@ describe('ResourceValidator', function () { (() => { mapDeclaration.accept(resourceValidator,parameters ); - }).should.throw('Model violation in the "TEST" instance. Invalid Type for Map Key or Value - expected String type.'); + }).should.throw('Model violation in org.acme.map.PhoneBook. Expected Type of String but found \'1\' instead'); }); }); From 116b180946f5392795a480a1679c1c1b9e5dda79 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 14:38:01 +0100 Subject: [PATCH 12/41] feat(map): cleanup Signed-off-by: jonathan.casey --- .../lib/serializer/jsonpopulator.js | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index fc846d05d..e7e681899 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -188,35 +188,39 @@ class JSONPopulator { } // If its a Non-Primitive, its likely a ClassDeclaration which needs visiting. - if (!ModelUtil.isSystemProperty(key) && !ModelUtil.isPrimitiveType(key)) { - // get the Key thing. - let thing = mapDeclaration.getModelFile() + if (!ModelUtil.isSystemProperty(key) && !ModelUtil.isPrimitiveType(mapDeclaration.getKey().getType())) { + // get the Key declaration. + let decl = mapDeclaration.getModelFile() .getAllDeclarations() .find(decl => decl.name === mapDeclaration.getKey().getType()); // parse the Object, and visit the declaration. - if (thing?.isClassDeclaration()) { - let subResource = parameters.factory.newConcept(thing.getNamespace(), - thing.getName(), thing.getIdentifierFieldName() ); + if (decl?.isClassDeclaration()) { + let subResource = parameters.factory.newConcept(decl.getNamespace(), + decl.getName(), decl.getIdentifierFieldName() ); + parameters.jsonStack.push(JSON.parse(key)); parameters.resourceStack.push(subResource); - key = thing.accept(this, parameters); + + key = decl.accept(this, parameters); } } if (!ModelUtil.isPrimitiveType(mapDeclaration.getValue().getType())) { - // get the Value thing. - let thing = mapDeclaration.getModelFile() + // get the Value declaration. + let decl = mapDeclaration.getModelFile() .getAllDeclarations() .find(decl => decl.name === mapDeclaration.getValue().getType()); // parse the Object, and visit the declaration. - if (thing?.isClassDeclaration() ) { - let subResource = parameters.factory.newConcept(thing.getNamespace(), - thing.getName(), thing.getIdentifierFieldName() ); + if (decl?.isClassDeclaration() ) { + let subResource = parameters.factory.newConcept(decl.getNamespace(), + decl.getName(), decl.getIdentifierFieldName() ); + parameters.jsonStack.push(JSON.parse(value)); parameters.resourceStack.push(subResource); - value = thing.accept(this, parameters); + + value = decl.accept(this, parameters); } } From 4478693f68096745c8fda5fc578caabc39f34c1b Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Tue, 1 Aug 2023 15:55:35 +0100 Subject: [PATCH 13/41] feat(map): cleanup test cases Signed-off-by: jonathan.casey --- packages/concerto-core/test/serializer.js | 124 ------------------ .../test/serializer/maptype/serializer.js | 87 ++++++++---- 2 files changed, 58 insertions(+), 153 deletions(-) diff --git a/packages/concerto-core/test/serializer.js b/packages/concerto-core/test/serializer.js index 226163d10..de416f7af 100644 --- a/packages/concerto-core/test/serializer.js +++ b/packages/concerto-core/test/serializer.js @@ -66,17 +66,6 @@ describe('Serializer', () => { o String country o Double elevation o PostalCode postcode optional - o Dictionary dict optional - } - - map Dictionary { - o String - o String - } - - map PhoneBook { - o String - o String } concept DateTimeTest { @@ -243,58 +232,6 @@ describe('Serializer', () => { }); }); - it('should generate concept with a Map value', () => { - let address = factory.newConcept('org.acme.sample', 'Address'); - address.city = 'Winchester'; - address.country = 'UK'; - address.elevation = 3.14; - address.postcode = 'SO21 2JN'; - address.dict = new Map(); - address.dict.set('$class', 'org.acme.sample.Dictionary'); - address.dict.set('Lorem', 'Ipsum'); - - // todo test for reserved identifiers in keys ($class) - const json = serializer.toJSON(address); - json.should.deep.equal({ - $class: 'org.acme.sample.Address', - country: 'UK', - elevation: 3.14, - city: 'Winchester', - postcode: 'SO21 2JN', - dict: { - $class: 'org.acme.sample.Dictionary', - Lorem: 'Ipsum' - } - }); - }); - - it('should throw if the value for a Map is not a Map instance', () => { - let address = factory.newConcept('org.acme.sample', 'Address'); - address.city = 'Winchester'; - address.country = 'UK'; - address.elevation = 3.14; - address.postcode = 'SO21 2JN'; - address.dict = 'xyz'; // bad value - (() => { - serializer.toJSON(address); - }).should.throw(`Expected a Map, but found ${JSON.stringify(address.dict)}`); - }); - - it('should throw validation error if there is a mismatch on map $class property', () => { - let address = factory.newConcept('org.acme.sample', 'Address'); - address.city = 'Winchester'; - address.country = 'UK'; - address.elevation = 3.14; - address.postcode = 'SO21 2JN'; - address.dict = new Map(); - address.dict.set('$class', 'org.acme.sample.PhoneBook'); // dict is not a PhoneBook. - address.dict.set('Lorem', 'Ipsum'); - - (() => { - serializer.toJSON(address); - }).should.throw('$class value must match org.acme.sample.Dictionary'); - }); - it('should generate a field if an empty string is specififed', () => { let resource = factory.newResource('org.acme.sample', 'SampleAsset', '1'); resource.owner = factory.newRelationship('org.acme.sample', 'SampleParticipant', 'alice@email.com'); @@ -395,67 +332,6 @@ describe('Serializer', () => { resource.postcode.should.equal('SO21 2JN'); }); - it('should deserialize a valid concept with a Map', () => { - let json = { - $class: 'org.acme.sample.Address', - city: 'Winchester', - country: 'UK', - elevation: 3.14, - postcode: 'SO21 2JN', - dict: { - '$class': 'org.acme.sample.Dictionary', - 'Lorem': 'Ipsum' - } - }; - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.city.should.equal('Winchester'); - resource.country.should.equal('UK'); - resource.elevation.should.equal(3.14); - resource.postcode.should.equal('SO21 2JN'); - resource.dict.should.be.an.instanceOf(Map); - resource.dict.get('$class').should.equal('org.acme.sample.Dictionary'); - resource.dict.get('Lorem').should.equal('Ipsum'); - }); - - it('should throw an error when deserializing a Map without a $class property', () => { - - let json = { - $class: 'org.acme.sample.Address', - city: 'Winchester', - country: 'UK', - elevation: 3.14, - postcode: 'SO21 2JN', - dict: { - // '$class': 'org.acme.sample.Dictionary', - 'Lorem': 'Ipsum' - } - }; - (() => { - serializer.fromJSON(json); - }).should.throw('Invalid Map. Map must contain a properly formatted $class property'); - }); - - - it('should throw an error when deserializing a Map with a private reserved property', () => { - - let json = { - $class: 'org.acme.sample.Address', - city: 'Winchester', - country: 'UK', - elevation: 3.14, - postcode: 'SO21 2JN', - dict: { - '$class': 'org.acme.sample.Dictionary', - '$namespace': 'com.reserved.property', - 'Lorem': 'Ipsum' - } - }; - (() => { - serializer.fromJSON(json); - }).should.throw('Unexpected reserved properties for type org.acme.sample.Dictionary: $namespace'); - }); it('should throw validation errors if the validate flag is not specified', () => { let json = { diff --git a/packages/concerto-core/test/serializer/maptype/serializer.js b/packages/concerto-core/test/serializer/maptype/serializer.js index ec8f5b272..68b31b838 100644 --- a/packages/concerto-core/test/serializer/maptype/serializer.js +++ b/packages/concerto-core/test/serializer/maptype/serializer.js @@ -216,6 +216,7 @@ describe('Serializer', () => { concept.dict = new Map(); concept.dict.set('$class', 'org.acme.sample.Dictionary'); concept.dict.set('Lorem', 'Ipsum'); + concept.dict.set('Ipsum', 'Lorem'); const json = serializer.toJSON(concept); @@ -223,7 +224,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', dict: { $class: 'org.acme.sample.Dictionary', - Lorem: 'Ipsum' + Lorem: 'Ipsum', + Ipsum: 'Lorem' } }); }); @@ -253,7 +255,8 @@ describe('Serializer', () => { concept.birthday = new Map(); concept.birthday.set('$class', 'org.acme.sample.Birthday'); - concept.birthday.set('Ipsum', '2023-10-28T01:02:03Z'); + concept.birthday.set('Lorem', '2023-10-28T01:02:03Z'); + concept.birthday.set('Ipsum', '2023-11-28T01:02:03Z'); const json = serializer.toJSON(concept); @@ -261,7 +264,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', birthday: { $class: 'org.acme.sample.Birthday', - 'Ipsum': '2023-10-28T01:02:03Z' + 'Lorem': '2023-10-28T01:02:03Z', + 'Ipsum': '2023-11-28T01:02:03Z' } }); }); @@ -269,18 +273,21 @@ describe('Serializer', () => { it('should generate a JSON object with a Map ', () => { let concept = factory.newConcept('org.acme.sample', 'Concepts'); - const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); concept.rolodex = new Map(); concept.rolodex.set('$class', 'org.acme.sample.Rolodex'); - concept.rolodex.set('Abbeyleix', person); + concept.rolodex.set('Abbeyleix', bob); + concept.rolodex.set('Ireland', alice); const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', rolodex: { $class: 'org.acme.sample.Rolodex', - 'Abbeyleix': '{"$class":"org.acme.sample.Person","name":"Bob"}' + 'Abbeyleix': '{"$class":"org.acme.sample.Person","name":"Bob"}', + 'Ireland': '{"$class":"org.acme.sample.Person","name":"Alice"}' } }); }); @@ -347,10 +354,12 @@ describe('Serializer', () => { let concept = factory.newConcept('org.acme.sample', 'Concepts'); const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); concept.graduation = new Map(); concept.graduation.set('$class', 'org.acme.sample.Graduation'); concept.graduation.set('2023-10-28T01:02:03Z', person); + concept.graduation.set('2023-11-28T01:02:03Z', alice); const json = serializer.toJSON(concept); @@ -358,7 +367,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', graduation: { $class: 'org.acme.sample.Graduation', - '2023-10-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Bob"}' + '2023-10-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Bob"}', + '2023-11-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Alice"}' } }); }); @@ -452,6 +462,7 @@ describe('Serializer', () => { concept.appointment = new Map(); concept.appointment.set('$class', 'org.acme.sample.Appointment'); concept.appointment.set('2023-10-28T01:02:03Z', 'Lorem'); + concept.appointment.set('2023-11-28T01:02:03Z', 'Ipsum'); const json = serializer.toJSON(concept); @@ -460,6 +471,7 @@ describe('Serializer', () => { appointment: { $class: 'org.acme.sample.Appointment', '2023-10-28T01:02:03Z': 'Lorem', + '2023-11-28T01:02:03Z': 'Ipsum', } }); }); @@ -467,11 +479,13 @@ describe('Serializer', () => { it('should generate a JSON object with a Map , where Scalar extends DateTime', () => { let concept = factory.newConcept('org.acme.sample', 'Concepts'); - const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); concept.meeting = new Map(); concept.meeting.set('$class', 'org.acme.sample.Meeting'); - concept.meeting.set('2023-10-28T01:02:03Z', person); + concept.meeting.set('2023-10-28T01:02:03Z', bob); + concept.meeting.set('2023-11-28T01:02:03Z', alice); const json = serializer.toJSON(concept); @@ -479,7 +493,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', meeting: { $class: 'org.acme.sample.Meeting', - '2023-10-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Bob"}' + '2023-10-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Bob"}', + '2023-11-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Alice"}' } }); }); @@ -510,11 +525,13 @@ describe('Serializer', () => { it('should generate a JSON object with a Map , where Concept is Identified & Scalar extends DateTime ', () => { let concepts = factory.newConcept('org.acme.sample', 'Concepts'); - const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); concepts.reservation = new Map(); concepts.reservation.set('$class', 'org.acme.sample.Reservation'); - concepts.reservation.set(person, '2023-10-28T01:02:03Z'); + concepts.reservation.set(bob, '2023-10-28T01:02:03Z'); + concepts.reservation.set(alice, '2023-11-28T01:02:03Z'); const json = serializer.toJSON(concepts); @@ -522,7 +539,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', reservation: { $class: 'org.acme.sample.Reservation', - '{"$class":"org.acme.sample.Person","name":"Bob"}' : '2023-10-28T01:02:03Z' + '{"$class":"org.acme.sample.Person","name":"Bob"}' : '2023-10-28T01:02:03Z', + '{"$class":"org.acme.sample.Person","name":"Alice"}' : '2023-11-28T01:02:03Z' } }); }); @@ -530,11 +548,13 @@ describe('Serializer', () => { it('should generate a JSON object with a Map , where Concept is Identified', () => { let concepts = factory.newConcept('org.acme.sample', 'Concepts'); - const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); concepts.vip = new Map(); concepts.vip.set('$class', 'org.acme.sample.GuestList'); - concepts.vip.set(person, true); + concepts.vip.set(bob, true); + concepts.vip.set(alice, true); const json = serializer.toJSON(concepts); @@ -542,7 +562,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', vip: { $class: 'org.acme.sample.GuestList', - '{"$class":"org.acme.sample.Person","name":"Bob"}' : true + '{"$class":"org.acme.sample.Person","name":"Bob"}' : true, + '{"$class":"org.acme.sample.Person","name":"Alice"}' : true } }); }); @@ -571,11 +592,13 @@ describe('Serializer', () => { it('should generate a JSON object with a Map , where Concept is Identified', () => { let concepts = factory.newConcept('org.acme.sample', 'Concepts'); - const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); concepts.graduated = new Map(); concepts.graduated.set('$class', 'org.acme.sample.Graduated'); - concepts.graduated.set(person, '2023-10-28T01:02:03Z'); + concepts.graduated.set(bob, '2023-10-28T01:02:03Z'); + concepts.graduated.set(alice, '2023-11-28T01:02:03Z'); const json = serializer.toJSON(concepts); @@ -583,7 +606,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', graduated: { $class: 'org.acme.sample.Graduated', - '{"$class":"org.acme.sample.Person","name":"Bob"}' : '2023-10-28T01:02:03Z' + '{"$class":"org.acme.sample.Person","name":"Bob"}' : '2023-10-28T01:02:03Z', + '{"$class":"org.acme.sample.Person","name":"Alice"}' : '2023-11-28T01:02:03Z' } }); }); @@ -681,7 +705,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', dict: { '$class': 'org.acme.sample.Dictionary', - 'Lorem': 'Ipsum' + 'Lorem': 'Ipsum', + 'Ipsum': 'Lorem' } }; let resource = serializer.fromJSON(json); @@ -697,7 +722,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', database: { '$class': 'org.acme.sample.Database', - 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': 'Ipsum' + 'E17B69D9-9B57-4C4A-957E-8B202D7B6C5A': 'Ipsum', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': 'Lorem' } }; let resource = serializer.fromJSON(json); @@ -705,7 +731,8 @@ describe('Serializer', () => { resource.should.be.an.instanceOf(Resource); resource.database.should.be.an.instanceOf(Map); resource.database.get('$class').should.equal('org.acme.sample.Database'); - resource.database.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').should.equal('Ipsum'); + resource.database.get('E17B69D9-9B57-4C4A-957E-8B202D7B6C5A').should.equal('Ipsum'); + resource.database.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').should.equal('Lorem'); }); it('should deserialize a JSON object with a Map , where Scalar extends DateTime', () => { @@ -713,7 +740,8 @@ describe('Serializer', () => { $class: 'org.acme.sample.Concepts', appointment: { '$class': 'org.acme.sample.Appointment', - '2023-10-28T01:02:03Z': 'Ipsum' + '2023-10-28T01:02:03Z': 'Ipsum', + '2023-11-28T01:02:03Z': 'Lorem' } }; let resource = serializer.fromJSON(json); @@ -737,7 +765,8 @@ describe('Serializer', () => { resource.should.be.an.instanceOf(Resource); resource.marriages.should.be.an.instanceOf(Map); - resource.marriages.get('$class', 'org.acme.sample.MarriageRegister'); + resource.marriages.get('$class').should.equal('org.acme.sample.MarriageRegister'); + resource.marriages.forEach((value, key) => { if (!ModelUtil.isSystemProperty(key)) { key.should.be.an.instanceOf(Resource); @@ -760,7 +789,7 @@ describe('Serializer', () => { resource.should.be.an.instanceOf(Resource); resource.grade.should.be.an.instanceOf(Map); - resource.grade.get('$class', 'org.acme.sample.ExaminationGrade'); + resource.grade.get('$class').should.equal('org.acme.sample.ExaminationGrade'); resource.grade.forEach((value, key) => { if (!ModelUtil.isSystemProperty(key)) { key.should.be.an.instanceOf(Resource); @@ -784,7 +813,7 @@ describe('Serializer', () => { resource.should.be.an.instanceOf(Resource); resource.graduated.should.be.an.instanceOf(Map); - resource.graduated.get('$class', 'org.acme.sample.Graduated'); + resource.graduated.get('$class').should.equal('org.acme.sample.Graduated'); resource.graduated.forEach((value, key) => { if (!ModelUtil.isSystemProperty(key)) { key.should.be.an.instanceOf(Resource); @@ -807,7 +836,7 @@ describe('Serializer', () => { resource.should.be.an.instanceOf(Resource); resource.reservation.should.be.an.instanceOf(Map); - resource.reservation.get('$class', 'org.acme.sample.Reservation'); + resource.reservation.get('$class').should.equal('org.acme.sample.Reservation'); resource.reservation.forEach((value, key) => { if (!ModelUtil.isSystemProperty(key)) { key.should.be.an.instanceOf(Resource); @@ -831,7 +860,7 @@ describe('Serializer', () => { resource.should.be.an.instanceOf(Resource); resource.vip.should.be.an.instanceOf(Map); - resource.vip.get('$class', 'org.acme.sample.GuestList'); + resource.vip.get('$class').should.equal('org.acme.sample.GuestList'); resource.vip.forEach((value, key) => { if (!ModelUtil.isSystemProperty(key)) { key.should.be.an.instanceOf(Resource); @@ -855,7 +884,7 @@ describe('Serializer', () => { resource.should.be.an.instanceOf(Resource); resource.team.should.be.an.instanceOf(Map); - resource.team.get('$class', 'org.acme.sample.Team'); + resource.team.get('$class').should.equal('org.acme.sample.Team'); resource.team.forEach((value, key) => { if (!ModelUtil.isSystemProperty(key)) { key.should.be.an.instanceOf(Resource); From ffe486a392d9648a1613ea514a52f3b842dc6384 Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Wed, 2 Aug 2023 09:55:44 +0100 Subject: [PATCH 14/41] test(map): add tests for missed cases Signed-off-by: Matt Roberts --- .../test/serializer/maptype/serializer.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/concerto-core/test/serializer/maptype/serializer.js b/packages/concerto-core/test/serializer/maptype/serializer.js index 68b31b838..4de307ea1 100644 --- a/packages/concerto-core/test/serializer/maptype/serializer.js +++ b/packages/concerto-core/test/serializer/maptype/serializer.js @@ -64,6 +64,7 @@ describe('Serializer', () => { o Meeting meeting optional o Graduation graduation optional o Team team optional + o StateLog stateLog optional } map Dictionary { @@ -199,6 +200,17 @@ describe('Serializer', () => { concept Leader { o String name } + + enum State { + o ON + o OFF + } + + map StateLog { + o State + o DateTime + } + `); factory = new Factory(modelManager); serializer = new Serializer(factory, modelManager); @@ -696,6 +708,27 @@ describe('Serializer', () => { serializer.toJSON(concept); }).should.throw('$class value must match org.acme.sample.Dictionary'); }); + + it('should ignore system properties', () => { + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + concept.dict = new Map(); + concept.dict.set('$class', 'org.acme.sample.Dictionary'); + concept.dict.set('$type', 'foo'); + concept.dict.set('Lorem', 'Ipsum'); + concept.dict.set('Ipsum', 'Lorem'); + + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + dict: { + $class: 'org.acme.sample.Dictionary', + Lorem: 'Ipsum', + Ipsum: 'Lorem' + } + }); + }); }); describe('#fromJSON', () => { @@ -921,6 +954,21 @@ describe('Serializer', () => { serializer.fromJSON(json); }).should.throw('Unexpected reserved properties for type org.acme.sample.Dictionary: $namespace'); }); + + it('should throw for Enums as Map key types', () => { + + let json = { + $class: 'org.acme.sample.Concepts', + stateLog: { + '$class': 'org.acme.sample.StateLog', + 'ON': '2000-01-01T00:00:00.000Z', + 'OFF': '2000-01-01T00:00:00.000Z', + } + }; + (() => { + serializer.fromJSON(json); + }).should.throw('TODO ADD CORRECT ERROR MESSAGE HERE'); + }); }); }); From 389449e384411f771b10063dec3c29c59b9c4e29 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 3 Aug 2023 16:49:40 +0100 Subject: [PATCH 15/41] feat(map): fixes metamodel parsing for MapDeclarations Signed-off-by: jonathan.casey --- packages/concerto-cto/lib/parser.js | 481 ++++++++++++++++++++++--- packages/concerto-cto/lib/parser.pegjs | 138 ++++++- 2 files changed, 558 insertions(+), 61 deletions(-) diff --git a/packages/concerto-cto/lib/parser.js b/packages/concerto-cto/lib/parser.js index a27ee0290..a41df2eae 100644 --- a/packages/concerto-cto/lib/parser.js +++ b/packages/concerto-cto/lib/parser.js @@ -994,7 +994,8 @@ function peg$parse(input, options) { const result = { $class: "concerto.metamodel@1.0.0.MapDeclaration", name: id.name, - properties: body.declarations, + key: body.declarations[0], + value: body.declarations[1], ...buildRange(location()) }; if (decorators.length > 0) { @@ -1008,32 +1009,116 @@ function peg$parse(input, options) { declarations: optionalList([key, value]) }; }; - var peg$f87 = function(decorators, id) { + var peg$f87 = function(decorators) { const result = { - $class: "concerto.metamodel@1.0.0.MapKeyType", - name: id.name, + $class: "concerto.metamodel@1.0.0.StringMapKeyType", ...buildRange(location()) }; + + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + }; + var peg$f88 = function(decorators) { + const result = { + $class: "concerto.metamodel@1.0.0.DateTimeMapKeyType", + ...buildRange(location()) + }; + + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + }; + var peg$f89 = function(decorators, propertyType) { + const result = { + $class: "concerto.metamodel@1.0.0.ObjectMapKeyType", + type: propertyType, + ...buildRange(location()) + }; + if (decorators.length > 0) { result.decorators = decorators; } return result; }; - var peg$f88 = function(decorators, symbol, id) { + var peg$f90 = function(decorators) { const result = { - $class: "concerto.metamodel@1.0.0.AggregateValueType", - name: id.name, + $class: "concerto.metamodel@1.0.0.BooleanMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + }; + var peg$f91 = function(decorators) { + const result = { + $class: "concerto.metamodel@1.0.0.DateTimeMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + }; + var peg$f92 = function(decorators) { + const result = { + $class: "concerto.metamodel@1.0.0.StringMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + }; + var peg$f93 = function(decorators) { + const result = { + $class: "concerto.metamodel@1.0.0.IntegerMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + }; + var peg$f94 = function(decorators) { + const result = { + $class: "concerto.metamodel@1.0.0.LongMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + }; + var peg$f95 = function(decorators) { + const result = { + $class: "concerto.metamodel@1.0.0.DoubleMapValueType", ...buildRange(location()) }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + }; + var peg$f96 = function(decorators, symbol, propertyType) { + const result = { + $class: "concerto.metamodel@1.0.0.ObjectMapValueType", + type: propertyType, + ...buildRange(location()) + }; + if(symbol === "-->") { - result.$class = "concerto.metamodel@1.0.0.AggregateRelationshipValueType"; + result.$class = "concerto.metamodel@1.0.0.RelationshipMapValueType"; } if (decorators.length > 0) { result.decorators = decorators; } return result; }; - var peg$f89 = function(decorators, id, body) { + var peg$f97 = function(decorators, id, body) { const result = { $class: "concerto.metamodel@1.0.0.EnumDeclaration", name: id.name, @@ -1045,13 +1130,13 @@ function peg$parse(input, options) { } return result; }; - var peg$f90 = function(decls) { + var peg$f98 = function(decls) { return { type: "EnumDeclarationBody", declarations: optionalList(decls) }; }; - var peg$f91 = function(decorators, id) { + var peg$f99 = function(decorators, id) { const result = { $class: "concerto.metamodel@1.0.0.EnumProperty", name: id.name, @@ -1062,7 +1147,7 @@ function peg$parse(input, options) { } return result; }; - var peg$f92 = function(decorators, propertyType, array, id, optional) { + var peg$f100 = function(decorators, propertyType, array, id, optional) { const result = { $class: "concerto.metamodel@1.0.0.RelationshipProperty", name: id.name, @@ -1076,22 +1161,22 @@ function peg$parse(input, options) { } return result; }; - var peg$f93 = function(first, rest) { + var peg$f101 = function(first, rest) { return first.concat(JSON.stringify(rest).replace(/['"]+/g, '')); }; - var peg$f94 = function(ns, version, name) { + var peg$f102 = function(ns, version, name) { return `${ns}@${version}.${name}`; }; - var peg$f95 = function(ns, version) { + var peg$f103 = function(ns, version) { return `${ns}@${version}`; }; - var peg$f96 = function(ns) { + var peg$f104 = function(ns) { return ns; }; - var peg$f97 = function(u) { + var peg$f105 = function(u) { return u; }; - var peg$f98 = function(ns, u) { + var peg$f106 = function(ns, u) { const result = { $class: "concerto.metamodel@1.0.0.ImportAll", namespace: ns, @@ -1099,7 +1184,7 @@ function peg$parse(input, options) { u && (result.uri = u); return result; }; - var peg$f99 = function(ns, u) { + var peg$f107 = function(ns, u) { const { namespace, name } = fullyQualifiedName(ns); const result = { $class: `${metamodelNamespace}.ImportType`, @@ -1109,7 +1194,7 @@ function peg$parse(input, options) { u && (result.uri = u); return result; }; - var peg$f100 = function(ns, types, u) { + var peg$f108 = function(ns, types, u) { const result = { $class: "concerto.metamodel@1.0.0.ImportTypes", namespace: ns, @@ -1118,13 +1203,13 @@ function peg$parse(input, options) { u && (result.uri = u); return result; }; - var peg$f101 = function(head, tail) { + var peg$f109 = function(head, tail) { return [head, ...tail]; }; - var peg$f102 = function(version) { + var peg$f110 = function(version) { return version.value; }; - var peg$f103 = function(version, decorators, ns, imports, body) { + var peg$f111 = function(version, decorators, ns, imports, body) { const result = { $class: "concerto.metamodel@1.0.0.Model", decorators: optionalList(decorators), @@ -1137,10 +1222,10 @@ function peg$parse(input, options) { } return result; }; - var peg$f104 = function(first, rest) { + var peg$f112 = function(first, rest) { return buildList(first, rest, 1); }; - var peg$f105 = function(first, rest) { + var peg$f113 = function(first, rest) { return buildList(first, rest, 1); }; var peg$currPos = 0; @@ -9802,10 +9887,10 @@ function peg$parse(input, options) { var s0, s1, s2, s3; s0 = peg$currPos; - s1 = peg$parseMapKeyTypeDeclaration(); + s1 = peg$parseMapKeyType(); if (s1 !== peg$FAILED) { s2 = peg$parse__(); - s3 = peg$parseAggregateValueTypeDeclaration(); + s3 = peg$parseMapValueType(); if (s3 !== peg$FAILED) { peg$savedPos = s0; s0 = peg$f86(s1, s3); @@ -9821,7 +9906,7 @@ function peg$parse(input, options) { return s0; } - function peg$parseMapKeyTypeDeclaration() { + function peg$parseStringMapKeyTypeDeclaration() { var s0, s1, s2, s3, s4, s5, s6; s0 = peg$currPos; @@ -9836,11 +9921,121 @@ function peg$parse(input, options) { } if (s3 !== peg$FAILED) { s4 = peg$parse__(); - s5 = peg$parseIdentifier(); + s5 = peg$parseStringType(); + if (s5 !== peg$FAILED) { + s6 = peg$parse__(); + peg$savedPos = s0; + s0 = peg$f87(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseDateTimeMapKeyTypeDeclaration() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseDecorators(); + s2 = peg$parse__(); + if (input.charCodeAt(peg$currPos) === 111) { + s3 = peg$c91; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + s5 = peg$parseDateTimeType(); + if (s5 !== peg$FAILED) { + s6 = peg$parse__(); + peg$savedPos = s0; + s0 = peg$f88(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseObjectMapKeyTypeDeclaration() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseDecorators(); + s2 = peg$parse__(); + if (input.charCodeAt(peg$currPos) === 111) { + s3 = peg$c91; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + s5 = peg$parseObjectType(); + if (s5 !== peg$FAILED) { + s6 = peg$parse__(); + peg$savedPos = s0; + s0 = peg$f89(s1, s5); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseMapKeyType() { + var s0; + + s0 = peg$parseStringMapKeyTypeDeclaration(); + if (s0 === peg$FAILED) { + s0 = peg$parseDateTimeMapKeyTypeDeclaration(); + if (s0 === peg$FAILED) { + s0 = peg$parseObjectMapKeyTypeDeclaration(); + } + } + + return s0; + } + + function peg$parseBooleanMapValueTypeDeclaration() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseDecorators(); + s2 = peg$parse__(); + if (input.charCodeAt(peg$currPos) === 111) { + s3 = peg$c91; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + s5 = peg$parseBooleanType(); if (s5 !== peg$FAILED) { s6 = peg$parse__(); peg$savedPos = s0; - s0 = peg$f87(s1, s5); + s0 = peg$f90(s1); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -9853,7 +10048,167 @@ function peg$parse(input, options) { return s0; } - function peg$parseAggregateValueTypeDeclaration() { + function peg$parseDateTimeMapValueTypeDeclaration() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseDecorators(); + s2 = peg$parse__(); + if (input.charCodeAt(peg$currPos) === 111) { + s3 = peg$c91; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + s5 = peg$parseDateTimeType(); + if (s5 !== peg$FAILED) { + s6 = peg$parse__(); + peg$savedPos = s0; + s0 = peg$f91(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseStringMapValueTypeDeclaration() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseDecorators(); + s2 = peg$parse__(); + if (input.charCodeAt(peg$currPos) === 111) { + s3 = peg$c91; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + s5 = peg$parseStringType(); + if (s5 !== peg$FAILED) { + s6 = peg$parse__(); + peg$savedPos = s0; + s0 = peg$f92(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseIntegerMapValueTypeDeclaration() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseDecorators(); + s2 = peg$parse__(); + if (input.charCodeAt(peg$currPos) === 111) { + s3 = peg$c91; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + s5 = peg$parseIntegerType(); + if (s5 !== peg$FAILED) { + s6 = peg$parse__(); + peg$savedPos = s0; + s0 = peg$f93(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseLongMapValueTypeDeclaration() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseDecorators(); + s2 = peg$parse__(); + if (input.charCodeAt(peg$currPos) === 111) { + s3 = peg$c91; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + s5 = peg$parseLongType(); + if (s5 !== peg$FAILED) { + s6 = peg$parse__(); + peg$savedPos = s0; + s0 = peg$f94(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseDoubleMapValueTypeDeclaration() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parseDecorators(); + s2 = peg$parse__(); + if (input.charCodeAt(peg$currPos) === 111) { + s3 = peg$c91; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parse__(); + s5 = peg$parseDoubleType(); + if (s5 !== peg$FAILED) { + s6 = peg$parse__(); + peg$savedPos = s0; + s0 = peg$f95(s1); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseObjectMapValueTypeDeclaration() { var s0, s1, s2, s3, s4, s5, s6; s0 = peg$currPos; @@ -9877,11 +10232,11 @@ function peg$parse(input, options) { } if (s3 !== peg$FAILED) { s4 = peg$parse__(); - s5 = peg$parseIdentifier(); + s5 = peg$parseObjectType(); if (s5 !== peg$FAILED) { s6 = peg$parse__(); peg$savedPos = s0; - s0 = peg$f88(s1, s3, s5); + s0 = peg$f96(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -9894,6 +10249,32 @@ function peg$parse(input, options) { return s0; } + function peg$parseMapValueType() { + var s0; + + s0 = peg$parseBooleanMapValueTypeDeclaration(); + if (s0 === peg$FAILED) { + s0 = peg$parseDateTimeMapValueTypeDeclaration(); + if (s0 === peg$FAILED) { + s0 = peg$parseStringMapValueTypeDeclaration(); + if (s0 === peg$FAILED) { + s0 = peg$parseIntegerMapValueTypeDeclaration(); + if (s0 === peg$FAILED) { + s0 = peg$parseLongMapValueTypeDeclaration(); + if (s0 === peg$FAILED) { + s0 = peg$parseDoubleMapValueTypeDeclaration(); + if (s0 === peg$FAILED) { + s0 = peg$parseObjectMapValueTypeDeclaration(); + } + } + } + } + } + } + + return s0; + } + function peg$parseEnumDeclaration() { var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11; @@ -9926,7 +10307,7 @@ function peg$parse(input, options) { } if (s11 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f89(s1, s5, s9); + s0 = peg$f97(s1, s5, s9); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -9958,7 +10339,7 @@ function peg$parse(input, options) { s2 = peg$parseEnumPropertyDeclaration(); } peg$savedPos = s0; - s1 = peg$f90(s1); + s1 = peg$f98(s1); s0 = s1; return s0; @@ -9983,7 +10364,7 @@ function peg$parse(input, options) { if (s5 !== peg$FAILED) { s6 = peg$parse__(); peg$savedPos = s0; - s0 = peg$f91(s1, s5); + s0 = peg$f99(s1, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10034,7 +10415,7 @@ function peg$parse(input, options) { } s12 = peg$parse__(); peg$savedPos = s0; - s0 = peg$f92(s1, s5, s7, s9, s11); + s0 = peg$f100(s1, s5, s7, s9, s11); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10112,7 +10493,7 @@ function peg$parse(input, options) { } s2 = input.substring(s2, peg$currPos); peg$savedPos = s0; - s0 = peg$f93(s1, s2); + s0 = peg$f101(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10160,7 +10541,7 @@ function peg$parse(input, options) { } if (s5 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f94(s1, s3, s5); + s0 = peg$f102(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10208,7 +10589,7 @@ function peg$parse(input, options) { } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f95(s1, s3); + s0 = peg$f103(s1, s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10258,7 +10639,7 @@ function peg$parse(input, options) { if (s3 !== peg$FAILED) { s4 = peg$parse__(); peg$savedPos = s0; - s0 = peg$f96(s3); + s0 = peg$f104(s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10288,7 +10669,7 @@ function peg$parse(input, options) { if (s3 !== peg$FAILED) { s4 = peg$parse__(); peg$savedPos = s0; - s0 = peg$f97(s3); + s0 = peg$f105(s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10326,7 +10707,7 @@ function peg$parse(input, options) { s7 = null; } peg$savedPos = s0; - s0 = peg$f98(s3, s7); + s0 = peg$f106(s3, s7); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10362,7 +10743,7 @@ function peg$parse(input, options) { s5 = null; } peg$savedPos = s0; - s0 = peg$f99(s3, s5); + s0 = peg$f107(s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10410,7 +10791,7 @@ function peg$parse(input, options) { s10 = null; } peg$savedPos = s0; - s0 = peg$f100(s3, s6, s10); + s0 = peg$f108(s3, s6, s10); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10509,7 +10890,7 @@ function peg$parse(input, options) { } } peg$savedPos = s0; - s0 = peg$f101(s1, s3); + s0 = peg$f109(s1, s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10546,7 +10927,7 @@ function peg$parse(input, options) { if (s5 !== peg$FAILED) { s6 = peg$parse__(); peg$savedPos = s0; - s0 = peg$f102(s5); + s0 = peg$f110(s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10583,7 +10964,7 @@ function peg$parse(input, options) { s5 = null; } peg$savedPos = s0; - s0 = peg$f103(s1, s2, s3, s4, s5); + s0 = peg$f111(s1, s2, s3, s4, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10623,7 +11004,7 @@ function peg$parse(input, options) { } } peg$savedPos = s0; - s0 = peg$f104(s1, s2); + s0 = peg$f112(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -10663,7 +11044,7 @@ function peg$parse(input, options) { } } peg$savedPos = s0; - s0 = peg$f105(s1, s2); + s0 = peg$f113(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; diff --git a/packages/concerto-cto/lib/parser.pegjs b/packages/concerto-cto/lib/parser.pegjs index 9dcbcfe04..a7d993837 100644 --- a/packages/concerto-cto/lib/parser.pegjs +++ b/packages/concerto-cto/lib/parser.pegjs @@ -1425,7 +1425,8 @@ MapDeclaration const result = { $class: "concerto.metamodel@1.0.0.MapDeclaration", name: id.name, - properties: body.declarations, + key: body.declarations[0], + value: body.declarations[1], ...buildRange(location()) }; if (decorators.length > 0) { @@ -1435,35 +1436,141 @@ MapDeclaration } MapDeclarationBody - = key:MapKeyTypeDeclaration __ value:AggregateValueTypeDeclaration { + = key:MapKeyType __ value:MapValueType { return { type: "MapDeclarationBody", declarations: optionalList([key, value]) }; } -MapKeyTypeDeclaration - = decorators:Decorators __ "o"__ id:Identifier __ { +StringMapKeyTypeDeclaration + = decorators:Decorators __ "o"__ StringType __ { const result = { - $class: "concerto.metamodel@1.0.0.MapKeyType", - name: id.name, + $class: "concerto.metamodel@1.0.0.StringMapKeyType", ...buildRange(location()) }; + if (decorators.length > 0) { result.decorators = decorators; } return result; } -AggregateValueTypeDeclaration - = decorators:Decorators __ symbol:("o" / "-->") __ id:Identifier __ { +DateTimeMapKeyTypeDeclaration + = decorators:Decorators __ "o"__ DateTimeType __ { + const result = { + $class: "concerto.metamodel@1.0.0.DateTimeMapKeyType", + ...buildRange(location()) + }; + + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + } + +ObjectMapKeyTypeDeclaration + = decorators:Decorators __ "o"__ propertyType:ObjectType __ { + const result = { + $class: "concerto.metamodel@1.0.0.ObjectMapKeyType", + type: propertyType, + ...buildRange(location()) + }; + + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + } + +MapKeyType = + StringMapKeyTypeDeclaration + / DateTimeMapKeyTypeDeclaration + / ObjectMapKeyTypeDeclaration + + +BooleanMapValueTypeDeclaration + = decorators:Decorators __ "o" __ BooleanType __ { const result = { - $class: "concerto.metamodel@1.0.0.AggregateValueType", - name: id.name, + $class: "concerto.metamodel@1.0.0.BooleanMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + } + +DateTimeMapValueTypeDeclaration + = decorators:Decorators __ "o" __ DateTimeType __ { + const result = { + $class: "concerto.metamodel@1.0.0.DateTimeMapValueType", ...buildRange(location()) }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + } + +StringMapValueTypeDeclaration + = decorators:Decorators __ "o" __ StringType __ { + const result = { + $class: "concerto.metamodel@1.0.0.StringMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + } + +IntegerMapValueTypeDeclaration + = decorators:Decorators __ "o" __ IntegerType __ { + const result = { + $class: "concerto.metamodel@1.0.0.IntegerMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + } + +LongMapValueTypeDeclaration + = decorators:Decorators __ "o" __ LongType __ { + const result = { + $class: "concerto.metamodel@1.0.0.LongMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + } + +DoubleMapValueTypeDeclaration + = decorators:Decorators __ "o" __ DoubleType __ { + const result = { + $class: "concerto.metamodel@1.0.0.DoubleMapValueType", + ...buildRange(location()) + }; + if (decorators.length > 0) { + result.decorators = decorators; + } + return result; + } + +ObjectMapValueTypeDeclaration + = decorators:Decorators __ symbol:("o" / "-->") __ propertyType:ObjectType __ { + const result = { + $class: "concerto.metamodel@1.0.0.ObjectMapValueType", + type: propertyType, + ...buildRange(location()) + }; + if(symbol === "-->") { - result.$class = "concerto.metamodel@1.0.0.AggregateRelationshipValueType"; + result.$class = "concerto.metamodel@1.0.0.RelationshipMapValueType"; } if (decorators.length > 0) { result.decorators = decorators; @@ -1471,6 +1578,15 @@ AggregateValueTypeDeclaration return result; } +MapValueType = + BooleanMapValueTypeDeclaration + / DateTimeMapValueTypeDeclaration + / StringMapValueTypeDeclaration + / IntegerMapValueTypeDeclaration + / LongMapValueTypeDeclaration + / DoubleMapValueTypeDeclaration + / ObjectMapValueTypeDeclaration + EnumDeclaration = decorators:Decorators __ EnumToken __ id:Identifier __ "{" __ body:EnumDeclarationBody __ "}" From bfbe382d46281384e4705e417a20c01bae5e8de5 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 3 Aug 2023 16:50:41 +0100 Subject: [PATCH 16/41] feat(map): printer update reads new metamodel map shape Signed-off-by: jonathan.casey --- packages/concerto-cto/lib/printer.js | 93 +++++++++++++++++++++------- 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/packages/concerto-cto/lib/printer.js b/packages/concerto-cto/lib/printer.js index 62751c5cf..3d4ab3798 100644 --- a/packages/concerto-cto/lib/printer.js +++ b/packages/concerto-cto/lib/printer.js @@ -16,6 +16,31 @@ const { MetaModelNamespace } = require('@accordproject/concerto-metamodel'); +/** + * Returns true if the metamodel is a MapDeclaration + * @param {object} mm - the metamodel + * @return {boolean} the string for that model + */ +function isMap(mm) { + return mm.$class === `${MetaModelNamespace}.MapDeclaration`; +} + +/** + * Returns true if the metamodel is a ScalarDeclaration + * @param {object} mm - the metamodel + * @return {boolean} the string for that model + */ +function isScalar(mm) { + return [ + `${MetaModelNamespace}.BooleanScalar`, + `${MetaModelNamespace}.IntegerScalar`, + `${MetaModelNamespace}.LongScalar`, + `${MetaModelNamespace}.DoubleScalar`, + `${MetaModelNamespace}.StringScalar`, + `${MetaModelNamespace}.DateTimeScalar`, + ].includes(mm.$class); +} + /** * Create decorator argument string from a metamodel * @param {object} mm - the metamodel @@ -79,32 +104,43 @@ function typeFromMetaModel(mm){ break; case `${MetaModelNamespace}.BooleanScalar`: case `${MetaModelNamespace}.BooleanProperty`: + case `${MetaModelNamespace}.BooleanMapValueType`: result += ' Boolean'; break; case `${MetaModelNamespace}.DateTimeProperty`: case `${MetaModelNamespace}.DateTimeScalar`: + case `${MetaModelNamespace}.DateTimeMapKeyType`: + case `${MetaModelNamespace}.DateTimeMapValueType`: result += ' DateTime'; break; case `${MetaModelNamespace}.DoubleProperty`: case `${MetaModelNamespace}.DoubleScalar`: + case `${MetaModelNamespace}.DoubleMapValueType`: result += ' Double'; break; case `${MetaModelNamespace}.IntegerProperty`: case `${MetaModelNamespace}.IntegerScalar`: + case `${MetaModelNamespace}.IntegerMapValueType`: result += ' Integer'; break; case `${MetaModelNamespace}.LongProperty`: case `${MetaModelNamespace}.LongScalar`: + case `${MetaModelNamespace}.LongMapValueType`: result += ' Long'; break; case `${MetaModelNamespace}.StringProperty`: case `${MetaModelNamespace}.StringScalar`: + case `${MetaModelNamespace}.StringMapKeyType`: + case `${MetaModelNamespace}.StringMapValueType`: result += ' String'; break; case `${MetaModelNamespace}.ObjectProperty`: + case `${MetaModelNamespace}.ObjectMapKeyType`: + case `${MetaModelNamespace}.ObjectMapValueType`: result += ` ${mm.type.name}`; break; case `${MetaModelNamespace}.RelationshipProperty`: + case `${MetaModelNamespace}.RelationshipMapValueType`: result += ` ${mm.type.name}`; break; } @@ -212,8 +248,7 @@ function propertyFromMetaModel(prop) { if (prop.decorators) { result += decoratorsFromMetaModel(prop.decorators, ' '); } - if (prop.$class === `${MetaModelNamespace}.RelationshipProperty` || - prop.$class === `${MetaModelNamespace}.AggregateRelationshipValueType`) { + if (prop.$class === `${MetaModelNamespace}.RelationshipProperty`) { result += '-->'; } else { result += 'o'; @@ -230,6 +265,28 @@ function propertyFromMetaModel(prop) { return result; } +/** + * Create a map type string from a metamodel map + * @param {object} entry - the map entry in scope + * @return {string} the CML string representation of the property + */ +function mapFromMetaModel(entry) { + let result = ''; + + if (entry.decorators) { + result += decoratorsFromMetaModel(entry.decorators, ' '); + } + if (entry.$class === `${MetaModelNamespace}.RelationshipMapValueType`) { + result += '-->'; + } else { + result += 'o'; + } + result += typeFromMetaModel(entry); + + return result; +} + + /** * Create a declaration string from a metamodel * @param {object} mm - the metamodel @@ -238,32 +295,23 @@ function propertyFromMetaModel(prop) { function declFromMetaModel(mm) { let result = ''; - const booleanScalar$class = `${MetaModelNamespace}.BooleanScalar`; - const integerScalar$class = `${MetaModelNamespace}.IntegerScalar`; - const longScalar$class = `${MetaModelNamespace}.LongScalar`; - const doubleScalar$class = `${MetaModelNamespace}.DoubleScalar`; - const stringScalar$class = `${MetaModelNamespace}.StringScalar`; - const dateTimeScalar$class = `${MetaModelNamespace}.DateTimeScalar`; - const scalar$classes = [ - booleanScalar$class, - integerScalar$class, - longScalar$class, - doubleScalar$class, - stringScalar$class, - dateTimeScalar$class, - ]; - const isScalar = scalar$classes.includes(mm.$class); - if (mm.decorators) { result += decoratorsFromMetaModel(mm.decorators, ''); } - if (isScalar) { + if (isScalar(mm)) { result += `scalar ${mm.name} extends`; - result += typeFromMetaModel(mm); result += modifiersFromMetaModel(mm); - } else { + } else if (isMap(mm)) { + const entries = [mm.key, mm.value]; + result += `map ${mm.name} {`; + entries.forEach(entry => { + result += `\n ${mapFromMetaModel(entry)}`; + }); + result += '\n}'; + } + else { if (mm.isAbstract) { result += 'abstract '; } @@ -286,9 +334,6 @@ function declFromMetaModel(mm) { case `${MetaModelNamespace}.EnumDeclaration`: result += `enum ${mm.name} `; break; - case `${MetaModelNamespace}.MapDeclaration`: - result += `map ${mm.name} `; - break; } if (mm.identified) { if (mm.identified.$class === `${MetaModelNamespace}.IdentifiedBy`) { From 95709851f5b12aff077f591291632f138a555c5e Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 3 Aug 2023 16:54:06 +0100 Subject: [PATCH 17/41] feat(map): updates test data for new metamodel design Signed-off-by: jonathan.casey --- packages/concerto-cto/test/cto/map.cto | 149 ++++- packages/concerto-cto/test/cto/map.json | 828 +++++++++++++++++------- 2 files changed, 707 insertions(+), 270 deletions(-) diff --git a/packages/concerto-cto/test/cto/map.cto b/packages/concerto-cto/test/cto/map.cto index 3dce4bec5..a23cb531f 100644 --- a/packages/concerto-cto/test/cto/map.cto +++ b/packages/concerto-cto/test/cto/map.cto @@ -1,33 +1,123 @@ namespace com.acme@1.0.0 -map Dictionary { +map MapPermutation1 { o String o String } -map Checklist { +map MapPermutation2 { o String o Boolean } -map Timeline { +map MapPermutation3 { + o String o DateTime - o Activity } -map AddressBook { - o GUID +map MapPermutation4 { + o String + o Integer +} + +map MapPermutation5 { + o String + o Long +} + +map MapPermutation6 { + o String + o Double +} + +map MapPermutation7 { + o String o Person } -map AddressBook2 { +map MapPermutation8 { + o DateTime + o Boolean +} + +map MapPermutation9 { + o DateTime + o DateTime +} + +map MapPermutation10 { + o DateTime + o String +} + +map MapPermutation11 { + o DateTime + o Integer +} + +map MapPermutation12 { + o DateTime + o Long +} + +map MapPermutation13 { + o DateTime + o Double +} + +map MapPermutation14 { + o DateTime + o Person +} + +map MapPermutation15 { + o Person + o String +} + +map MapPermutation16 { + o Person + o Boolean +} + +map MapPermutation17 { + o Person + o DateTime +} + +map MapPermutation18 { + o Person + o Integer +} + +map MapPermutation19 { + o Person + o Long +} + +map MapPermutation20 { + o Person + o Double +} + +map MapPermutation21 { + o Person + o Person +} + +map MapPermutation22 { o GUID --> Person } -map StateMachine { - o Phase +map MapPermutation23 { o String + --> Person +} + +map MapPermutation24 { + o DateTime + --> Person } @Foo("Alexandria") @@ -35,24 +125,33 @@ map Library { @Bar() o String @Baz() - o Dictionary -} - -map MarriageRegister { - o Person - o Person + o Book } concept Concept { - --> String test - o Dictionary dictionary - o Checklist checklist - o Timeline timeline - o AddressBook addressBook - o AddressBook remoteEmployees - o StateMachine state - o Dictionary[] arrayOfRecords - o Dictionary optionalRecord optional + o MapPermutation1 p1 + o MapPermutation2 p2 + o MapPermutation3 p3 + o MapPermutation4 p4 + o MapPermutation5 p5 + o MapPermutation6 p6 + o MapPermutation7 p7 + o MapPermutation8 p8 + o MapPermutation9 p9 + o MapPermutation10 p10 + o MapPermutation11 p11 + o MapPermutation12 p12 + o MapPermutation13 p13 + o MapPermutation14 p14 + o MapPermutation15 p15 + o MapPermutation16 p16 + o MapPermutation17 p17 + o MapPermutation18 p18 + o MapPermutation19 p19 + o MapPermutation20 p20 + o MapPermutation21 p21 + o MapPermutation22 p22 + o MapPermutation23 p23 + o MapPermutation24 p24 o Library library - o MarriageRegister marriages } diff --git a/packages/concerto-cto/test/cto/map.json b/packages/concerto-cto/test/cto/map.json index ae1b8d038..51e7a4b71 100644 --- a/packages/concerto-cto/test/cto/map.json +++ b/packages/concerto-cto/test/cto/map.json @@ -4,260 +4,598 @@ "namespace": "com.acme@1.0.0", "imports": [], "declarations": [ - { - "$class": "concerto.metamodel@1.0.0.MapDeclaration", - "name": "Dictionary", - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.MapKeyType", - "name": "String" - }, - { - "$class": "concerto.metamodel@1.0.0.AggregateValueType", - "name": "String" - } - ] - }, - { - "$class": "concerto.metamodel@1.0.0.MapDeclaration", - "name": "Checklist", - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.MapKeyType", - "name": "String" - }, - { - "$class": "concerto.metamodel@1.0.0.AggregateValueType", - "name": "Boolean" - } - ] - }, - { - "$class": "concerto.metamodel@1.0.0.MapDeclaration", - "name": "Timeline", - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.MapKeyType", - "name": "DateTime" - }, - { - "$class": "concerto.metamodel@1.0.0.AggregateValueType", - "name": "Activity" - } - ] - }, - { - "$class": "concerto.metamodel@1.0.0.MapDeclaration", - "name": "AddressBook", - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.MapKeyType", - "name": "GUID" - }, - { - "$class": "concerto.metamodel@1.0.0.AggregateValueType", - "name": "Person" - } - ] - }, - { - "$class": "concerto.metamodel@1.0.0.MapDeclaration", - "name": "AddressBook2", - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.MapKeyType", - "name": "GUID" - }, - { - "$class": "concerto.metamodel@1.0.0.AggregateRelationshipValueType", - "name": "Person" - } - ] - }, - { - "$class": "concerto.metamodel@1.0.0.MapDeclaration", - "name": "StateMachine", - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.MapKeyType", - "name": "Phase" - }, - { - "$class": "concerto.metamodel@1.0.0.AggregateValueType", - "name": "String" - } - ] - }, - { - "$class": "concerto.metamodel@1.0.0.MapDeclaration", - "name": "Library", - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.MapKeyType", - "name": "String", - "decorators": [ - { - "$class": "concerto.metamodel@1.0.0.Decorator", - "name": "Bar", - "arguments": [] - } - ] - }, - { - "$class": "concerto.metamodel@1.0.0.AggregateValueType", - "name": "Dictionary", - "decorators": [ - { - "$class": "concerto.metamodel@1.0.0.Decorator", - "name": "Baz", - "arguments": [] - } - ] - } - ], - "decorators": [ - { - "$class": "concerto.metamodel@1.0.0.Decorator", - "name": "Foo", - "arguments": [ - { - "$class": "concerto.metamodel@1.0.0.DecoratorString", - "value": "Alexandria" - } - ] - } - ] - }, - { - "$class": "concerto.metamodel@1.0.0.MapDeclaration", - "name": "MarriageRegister", - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.MapKeyType", - "name": "Person" - }, - { - "$class": "concerto.metamodel@1.0.0.AggregateValueType", - "name": "Person" - } - ] - }, - { - "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", - "name": "Concept", - "isAbstract": false, - "properties": [ - { - "$class": "concerto.metamodel@1.0.0.RelationshipProperty", - "name": "test", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "String" + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation1", + "key": { + "$class": "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "value": { + "$class": "concerto.metamodel@1.0.0.StringMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation2", + "key": { + "$class": "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "value": { + "$class": "concerto.metamodel@1.0.0.BooleanMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation3", + "key": { + "$class": "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "value": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation4", + "key": { + "$class": "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "value": { + "$class": "concerto.metamodel@1.0.0.IntegerMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation5", + "key": { + "$class": "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "value": { + "$class": "concerto.metamodel@1.0.0.LongMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation6", + "key": { + "$class": "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "value": { + "$class": "concerto.metamodel@1.0.0.DoubleMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation7", + "key": { + "$class": "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "value": { + "$class": "concerto.metamodel@1.0.0.ObjectMapValueType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation8", + "key": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapKeyType" }, - "isArray": false, - "isOptional": false - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "dictionary", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "Dictionary" + "value": { + "$class": "concerto.metamodel@1.0.0.BooleanMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation9", + "key": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapKeyType" }, - "isArray": false, - "isOptional": false - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "checklist", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "Checklist" + "value": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation10", + "key": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapKeyType" }, - "isArray": false, - "isOptional": false - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "timeline", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "Timeline" + "value": { + "$class": "concerto.metamodel@1.0.0.StringMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation11", + "key": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapKeyType" }, - "isArray": false, - "isOptional": false - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "addressBook", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "AddressBook" + "value": { + "$class": "concerto.metamodel@1.0.0.IntegerMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation12", + "key": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapKeyType" }, - "isArray": false, - "isOptional": false - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "remoteEmployees", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "AddressBook" + "value": { + "$class": "concerto.metamodel@1.0.0.LongMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation13", + "key": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapKeyType" }, - "isArray": false, - "isOptional": false - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "state", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "StateMachine" + "value": { + "$class": "concerto.metamodel@1.0.0.DoubleMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation14", + "key": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapKeyType" }, - "isArray": false, - "isOptional": false - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "arrayOfRecords", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "Dictionary" + "value": { + "$class": "concerto.metamodel@1.0.0.ObjectMapValueType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation15", + "key": { + "$class": "concerto.metamodel@1.0.0.ObjectMapKeyType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } }, - "isArray": true, - "isOptional": false - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "optionalRecord", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "Dictionary" + "value": { + "$class": "concerto.metamodel@1.0.0.StringMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation16", + "key": { + "$class": "concerto.metamodel@1.0.0.ObjectMapKeyType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } }, - "isArray": false, - "isOptional": true - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "library", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "Library" + "value": { + "$class": "concerto.metamodel@1.0.0.BooleanMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation17", + "key": { + "$class": "concerto.metamodel@1.0.0.ObjectMapKeyType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } }, - "isArray": false, - "isOptional": false - }, - { - "$class": "concerto.metamodel@1.0.0.ObjectProperty", - "name": "marriages", - "type": { - "$class": "concerto.metamodel@1.0.0.TypeIdentifier", - "name": "MarriageRegister" + "value": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation18", + "key": { + "$class": "concerto.metamodel@1.0.0.ObjectMapKeyType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } }, - "isArray": false, - "isOptional": false - } - ] - } + "value": { + "$class": "concerto.metamodel@1.0.0.IntegerMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation19", + "key": { + "$class": "concerto.metamodel@1.0.0.ObjectMapKeyType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } + }, + "value": { + "$class": "concerto.metamodel@1.0.0.LongMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation20", + "key": { + "$class": "concerto.metamodel@1.0.0.ObjectMapKeyType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } + }, + "value": { + "$class": "concerto.metamodel@1.0.0.DoubleMapValueType" + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation21", + "key": { + "$class": "concerto.metamodel@1.0.0.ObjectMapKeyType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } + }, + "value": { + "$class": "concerto.metamodel@1.0.0.ObjectMapValueType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation22", + "key": { + "$class": "concerto.metamodel@1.0.0.ObjectMapKeyType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "GUID" + } + }, + "value": { + "$class": "concerto.metamodel@1.0.0.RelationshipMapValueType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation23", + "key": { + "$class": "concerto.metamodel@1.0.0.StringMapKeyType" + }, + "value": { + "$class": "concerto.metamodel@1.0.0.RelationshipMapValueType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "MapPermutation24", + "key": { + "$class": "concerto.metamodel@1.0.0.DateTimeMapKeyType" + }, + "value": { + "$class": "concerto.metamodel@1.0.0.RelationshipMapValueType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Person" + } + } + }, + { + "$class": "concerto.metamodel@1.0.0.MapDeclaration", + "name": "Library", + "key": { + "$class": "concerto.metamodel@1.0.0.StringMapKeyType", + "decorators": [ + { + "$class": "concerto.metamodel@1.0.0.Decorator", + "name": "Bar", + "arguments": [] + } + ] + }, + "value": { + "$class": "concerto.metamodel@1.0.0.ObjectMapValueType", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Book" + }, + "decorators": [ + { + "$class": "concerto.metamodel@1.0.0.Decorator", + "name": "Baz", + "arguments": [] + } + ] + }, + "decorators": [ + { + "$class": "concerto.metamodel@1.0.0.Decorator", + "name": "Foo", + "arguments": [ + { + "$class": "concerto.metamodel@1.0.0.DecoratorString", + "value": "Alexandria" + } + ] + } + ] + }, + { + "$class": "concerto.metamodel@1.0.0.ConceptDeclaration", + "name": "Concept", + "isAbstract": false, + "properties": [ + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p1", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation1" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p2", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation2" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p3", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation3" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p4", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation4" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p5", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation5" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p6", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation6" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p7", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation7" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p8", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation8" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p9", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation9" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p10", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation10" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p11", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation11" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p12", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation12" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p13", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation13" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p14", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation14" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p15", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation15" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p16", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation16" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p17", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation17" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p18", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation18" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p19", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation19" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p20", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation20" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p21", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation21" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p22", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation22" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p23", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation23" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "p24", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "MapPermutation24" + }, + "isArray": false, + "isOptional": false + }, + { + "$class": "concerto.metamodel@1.0.0.ObjectProperty", + "name": "library", + "type": { + "$class": "concerto.metamodel@1.0.0.TypeIdentifier", + "name": "Library" + }, + "isArray": false, + "isOptional": false + } + ] + } ] - } +} From 57408b5071fbd93e65fcc26f5eb8b615a5e8885f Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Fri, 4 Aug 2023 22:41:27 +0100 Subject: [PATCH 18/41] feat(map): initial rework of introspection Signed-off-by: jonathan.casey --- .../lib/introspect/mapdeclaration.js | 60 ++- .../lib/introspect/mapkeytype.js | 72 ++- .../lib/introspect/mapvaluetype.js | 69 ++- ...pdeclaration.badvalue.declaration.map.cto} | 0 ...ion.badvalue.declaration.relationship.cto} | 0 .../mapdeclaration.badvalue.nontype.cto | 4 - .../test/introspect/mapdeclaration.js | 451 ++++++++++-------- 7 files changed, 392 insertions(+), 264 deletions(-) rename packages/concerto-core/test/data/parser/mapdeclaration/{mapdeclaration.goodvalue.declaration.map.cto => mapdeclaration.badvalue.declaration.map.cto} (100%) rename packages/concerto-core/test/data/parser/mapdeclaration/{mapdeclaration.goodvalue.declaration.relationship.cto => mapdeclaration.badvalue.declaration.relationship.cto} (100%) diff --git a/packages/concerto-core/lib/introspect/mapdeclaration.js b/packages/concerto-core/lib/introspect/mapdeclaration.js index 28823c877..915fa24c3 100644 --- a/packages/concerto-core/lib/introspect/mapdeclaration.js +++ b/packages/concerto-core/lib/introspect/mapdeclaration.js @@ -65,24 +65,21 @@ class MapDeclaration extends Declaration { process() { super.process(); - if (this.ast.properties.length !== 2) { - throw new IllegalModelException(`MapDeclaration must contain exactly two properties - MapKeyType & MapyValueType ${this.ast.name}`, this.modelFile, this.ast.location); + if (!this.ast.key || !this.ast.value) { + throw new IllegalModelException(`MapDeclaration must contain Key & Value properties ${this.ast.name}`, this.modelFile, this.ast.location); } - const key = this.ast.properties.find(property => property.$class === `${MetaModelNamespace}.MapKeyType`); - const value = this.ast.properties.find(property => property.$class === `${MetaModelNamespace}.AggregateValueType` || property.$class === `${MetaModelNamespace}.AggregateRelationshipValueType`); - - if (!key) { - throw new IllegalModelException(`MapDeclaration must contain MapKeyType ${this.ast.name}`, this.modelFile, this.ast.location); + if (!this.isValidMapKey(this.ast.key)) { + throw new IllegalModelException(`MapDeclaration must contain valid MapKeyType ${this.ast.name}`, this.modelFile, this.ast.location); } - if (!value) { - throw new IllegalModelException(`MapDeclaration must contain AggregateValueType ${this.ast.name}`, this.modelFile, this.ast.location); + if (!this.isValidMapValue(this.ast.value)) { + throw new IllegalModelException(`MapDeclaration must contain valid MapValueType, for MapDeclaration ${this.ast.name}` , this.modelFile, this.ast.location); } this.name = this.ast.name; - this.key = new MapKeyType(this, key); - this.value = new MapValueType(this, value); + this.key = new MapKeyType(this, this.ast.key); + this.value = new MapValueType(this, this.ast.value); this.fqn = ModelUtil.getFullyQualifiedName(this.modelFile.getNamespace(), this.ast.name); } @@ -146,15 +143,6 @@ class MapDeclaration extends Declaration { return this.value; } - /** - * Returns the MapDeclaration properties - * - * @return {array} the MapDeclaration properties - */ - getProperties() { - return this.ast.properties; - } - /** * Returns the string representation of this class * @return {String} the string representation of the class @@ -180,6 +168,38 @@ class MapDeclaration extends Declaration { isMapDeclaration() { return true; } + + // TODO MAKE PRIVATE + /** + * Returns true if this class is the definition of a class declaration. + * @param {MapDeclaration} key - the Key for the Map Declaration + * @return {boolean} true if the class is a class + */ + isValidMapKey(key) { + return [ + `${MetaModelNamespace}.StringMapKeyType`, + `${MetaModelNamespace}.DateTimeMapKeyType`, + `${MetaModelNamespace}.ObjectMapKeyType`, + ].includes(key.$class); + } + + // TODO MAKE PRIVATE + /** + * Returns true if this class is the definition of a class declaration. + * @param {MapDeclaration} value - the Key for the Map Declaration + * @return {boolean} true if the class is a class + */ + isValidMapValue(value) { + return [ + `${MetaModelNamespace}.BooleanMapValueType`, + `${MetaModelNamespace}.DateTimeMapValueType`, + `${MetaModelNamespace}.StringMapValueType`, + `${MetaModelNamespace}.IntegerMapValueType`, + `${MetaModelNamespace}.LongMapValueType`, + `${MetaModelNamespace}.DoubleMapValueType`, + `${MetaModelNamespace}.ObjectMapValueType` + ].includes(value.$class); + } } module.exports = MapDeclaration; diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index bc1fecfdd..1d260ac1a 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -14,6 +14,7 @@ 'use strict'; +const ModelUtil = require('../modelutil'); const { MetaModelNamespace } = require('@accordproject/concerto-metamodel'); const Decorated = require('./decorated'); @@ -46,7 +47,6 @@ class MapKeyType extends Decorated { constructor(parent, ast) { super(ast); this.parent = parent; - this.type = this.ast.name; this.process(); } @@ -58,6 +58,7 @@ class MapKeyType extends Decorated { */ process() { super.process(); + this.#processType(this.ast); } /** @@ -67,30 +68,59 @@ class MapKeyType extends Decorated { * @protected */ validate() { - const declaration = this.getModelFile().getAllDeclarations(); - const key = declaration.find(decl => decl.name === this.type); - - if (!key?.isConcept?.() && - !key?.isEnum?.() && - !key?.isScalarDeclaration?.() && - !['String', 'DateTime'].includes(this.type)) { - throw new IllegalModelException(`MapKeyType has invalid Type: ${this.type}`); - } - if (key?.isConcept?.() && !key.isIdentified()) { - throw new IllegalModelException( - `ConceptDeclaration must be identified in context of MapKeyType: ${this.type}` - ); - } - if (key?.isScalarDeclaration?.() && - !(key?.ast.$class === `${MetaModelNamespace}.StringScalar`) && - !(key?.ast.$class === `${MetaModelNamespace}.DateTimeScalar` )) { - throw new IllegalModelException( - `Scalar must be one of StringScalar, DateTimeScalar in context of MapKeyType. Invalid Scalar: ${this.type}` - ); + // TODO Test cases - illegal Primitives. + if (!ModelUtil.isPrimitiveType(this.type)) { + let decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type?.name); + + if (decl?.isScalarDeclaration?.() && + !(decl?.ast.$class === `${MetaModelNamespace}.StringScalar`) && + !(decl?.ast.$class === `${MetaModelNamespace}.DateTimeScalar`)) { + throw new IllegalModelException( + `Scalar must be one of StringScalar, DateTimeScalar in context of MapKeyType. Invalid Scalar: ${this.type}` + ); + } + + if(decl.isMapDeclaration?.()) { + throw new IllegalModelException( + `MapDeclaration as Map Type Value is not supported: ${this.type}` + ); + } + + if (decl?.isConcept?.() || decl?.isClassDeclaration?.()) { + throw new IllegalModelException( + `Concept as Map Type Key is not supported: ${this.type}` + ); + } + } + } + /** + * Sets the Type name for the Map Key + * + * @private + * @param {Object} ast - The AST created by the parser + */ + #processType(ast) { + let decl; + switch(this.ast.$class) { + case `${MetaModelNamespace}.DateTimeMapKeyType`: + this.type = 'DateTime'; + break; + case `${MetaModelNamespace}.StringMapKeyType`: + this.type = 'String'; + break; + case `${MetaModelNamespace}.ObjectMapKeyType`: + decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type.name); + this.type = decl.getName(); + break; + default: + decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type.name); + this.type = decl.getName(); + break; + } } /** diff --git a/packages/concerto-core/lib/introspect/mapvaluetype.js b/packages/concerto-core/lib/introspect/mapvaluetype.js index 7359c1d85..db0b72a11 100644 --- a/packages/concerto-core/lib/introspect/mapvaluetype.js +++ b/packages/concerto-core/lib/introspect/mapvaluetype.js @@ -15,7 +15,10 @@ 'use strict'; const Decorated = require('./decorated'); +const { MetaModelNamespace } = require('@accordproject/concerto-metamodel'); const IllegalModelException = require('../../lib/introspect/illegalmodelexception'); +const ModelUtil = require('../modelutil'); + // Types needed for TypeScript generation. /* eslint-disable no-unused-vars */ @@ -43,7 +46,6 @@ class MapValueType extends Decorated { constructor(parent, ast) { super(ast); this.parent = parent; - this.type = this.ast.name; this.process(); } @@ -55,6 +57,7 @@ class MapValueType extends Decorated { */ process() { super.process(); + this.#processType(this.ast); } /** @@ -64,23 +67,53 @@ class MapValueType extends Decorated { * @protected */ validate() { - const declarations = this.getModelFile().getAllDeclarations(); - - const value = declarations.find(decl => decl.name === this.type); - - if (!value?.isConcept?.() && - !value?.isEnum?.() && - !value?.isAsset?.() && - !value?.isEvent?.() && - !value?.isParticipant?.() && - !value?.isTransaction?.() && - !value?.isMapDeclaration?.() && - !value?.isScalarDeclaration?.() && - !['String', 'Long', 'Integer', 'Double', 'Boolean', 'DateTime'].includes(this.type)) { - - throw new IllegalModelException( - `MapPropertyType has invalid Type: ${this.type}` - ); + if (!ModelUtil.isPrimitiveType(this.type)) { + let decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type?.name); + + // All declarations, with the exception of MapDeclarations are valid Values. + if(decl.isMapDeclaration?.()) { + throw new IllegalModelException( + `MapDeclaration as Map Type Value is not supported: ${this.type}` + ); + } + } + } + + /** + * Sets the Type name for the Map Value + * + * @private + * @param {Object} ast - The AST created by the parser + */ + #processType(ast) { + let decl; + switch(this.ast.$class) { + case `${MetaModelNamespace}.ObjectMapValueType`: + decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type.name); + this.type = decl.getName(); + break; + // case 'RelationshipMapValueType': + // // todo - how to handle relationship?? + // this.type = 'Boolean'; + // break; + case `${MetaModelNamespace}.BooleanMapValueType`: + this.type = 'Boolean'; + break; + case `${MetaModelNamespace}.DateTimeMapValueType`: + this.type = 'DateTime'; + break; + case `${MetaModelNamespace}.StringMapValueType`: + this.type = 'String'; + break; + case `${MetaModelNamespace}.IntegerMapValueType`: + this.type = 'Integer'; + break; + case `${MetaModelNamespace}.LongMapValueType`: + this.type = 'Long'; + break; + case `${MetaModelNamespace}.DoubleMapValueType`: + this.type = 'Double'; + break; } } diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.map.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badvalue.declaration.map.cto similarity index 100% rename from packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.map.cto rename to packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badvalue.declaration.map.cto diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.relationship.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badvalue.declaration.relationship.cto similarity index 100% rename from packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.relationship.cto rename to packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badvalue.declaration.relationship.cto diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badvalue.nontype.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badvalue.nontype.cto index 3b7c95230..9b2623e96 100644 --- a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badvalue.nontype.cto +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badvalue.nontype.cto @@ -14,10 +14,6 @@ namespace com.acme@1.0.0 -enum Phase { - o ONE - o TWO -} map Dictionary { o Phase o NONTYPE diff --git a/packages/concerto-core/test/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index e473d0824..900792e83 100644 --- a/packages/concerto-core/test/introspect/mapdeclaration.js +++ b/packages/concerto-core/test/introspect/mapdeclaration.js @@ -45,55 +45,26 @@ describe('MapDeclaration', () => { describe('#constructor', () => { - it('should throw if ast contains no MapKeyType', () => { + it('should throw if ast contains no Map Key Property', () => { (() => { new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - name: 'String' - } - ] - }); - }).should.throw(IllegalModelException); - }); - - it('should throw if ast contains no MapValueType', () => { - (() => { - new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - name: 'Integer' - }, - { - '$class': 'concerto.metamodel@1.0.0.SomeRandomInvalidType', - name: 'String' - },] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } }); }).should.throw(IllegalModelException); }); - it('should throw if ast does not contain exactly two properties', () => { + it('should throw if ast contains no Map Value Property', () => { (() => { new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - name: 'String' - }, - { - '$class': 'concerto.metamodel@1.0.0.AggregateValueType', - name: 'String' - }, - { - '$class': 'concerto.metamodel@1.0.0.StringProperty', - name: 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + } }); }).should.throw(IllegalModelException); }); @@ -103,36 +74,54 @@ describe('MapDeclaration', () => { (() => { new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - name: 'String' - }, - { - '$class': 'concerto.metamodel@1.0.0.AggregateValueType', - name: 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } }); }).should.throw(/MapType feature is not enabled. Please set the environment variable "ENABLE_MAP_TYPE=true" to access this functionality./); process.env.ENABLE_MAP_TYPE = 'true'; // enable after the test run. This is necessary to ensure functioning of other tests. }); - it('should throw if ast contains properties other than MapKeyType, AggregateValueType & AggregateRelationshipValueType', () => { - (() => { + // should throw if ast contains $class for KEY other than + // should throw if ast contains $class for VALUE other than + + // should NOT throw if ast contains $class for KEY other than + // should NOT throw if ast contains $class for VALUE other than + + it('should throw if invalid $class provided for Map Key', () => { + (() => + { new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.StringProperty', - name: 'String' - }, - { - '$class': 'concerto.metamodel@1.0.0.StringProperty', - name: 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.BadMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + }).should.throw(IllegalModelException); + }); + + + it('should throw if invalid $class provided for Map Value', () => { + (() => + { + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.BadMapValueType' + } }); }).should.throw(IllegalModelException); }); @@ -140,15 +129,15 @@ describe('MapDeclaration', () => { describe('#validate success scenarios', () => { - it('should not throw when map key is an identified concept declaration', () => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto', MapDeclaration); - decl.validate(); - }); + // it('should not throw when map key is an identified concept declaration', () => { + // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto', MapDeclaration); + // decl.validate(); + // }); - it('should not throw when map key is an enum declaration', () => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.enum.cto', MapDeclaration); - decl.validate(); - }); + // it('should not throw when map key is an enum declaration', () => { + // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.enum.cto', MapDeclaration); + // decl.validate(); + // }); it('should not throw when map key is primitive type datetime', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.primitive.datetime.cto', MapDeclaration); @@ -200,15 +189,6 @@ describe('MapDeclaration', () => { decl.validate(); }); - it('should not throw when map value is a map declaration', () => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.map.cto', MapDeclaration); - decl.validate(); - }); - - it('should not throw when map value is a relationship', () => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.relationship.cto', MapDeclaration); - decl.validate(); - }); it('should not throw when map value is a primitive string', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.string.cto', MapDeclaration); @@ -238,27 +218,50 @@ describe('MapDeclaration', () => { }); describe('#validate failure scenarios', () => { - it('should throw validating with a non-identified concept declaration as key', function() { + + it('should throw when map key is a concept declaration', function() { (() => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.concept.cto', MapDeclaration); decl.validate(); - }).should.throw(/ConceptDeclaration must be identified in context of MapKeyType: NotIdentified/); + }).should.throw(/Concept as Map Type Key is not supported:/); }); - it('should throw when an enum key declaration missing', function() { + it('should throw when map value is a relationship', function() { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.enum.cto', MapDeclaration); + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badvalue.declaration.relationship.cto', MapDeclaration); decl.validate(); - }).should.throw(/MapKeyType has invalid Type: NotDeclared/); + }).should.throw(/MapDeclaration must contain valid MapValueType, for MapDeclaration Dictionary/); }); - it('should throw when map value is an illegal type', function() { + it('should throw when map value is a map declaration', function() { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badvalue.nontype.cto', MapDeclaration); + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badvalue.declaration.map.cto', MapDeclaration); decl.validate(); - }).should.throw(/MapPropertyType has invalid Type: NONTYPE/); + }).should.throw(/MapDeclaration as Map Type Value is not supported:/); }); + + // it('should throw validating with a non-identified concept declaration as key', function() { + // (() => { + // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.concept.cto', MapDeclaration); + // decl.validate(); + // }).should.throw(/ConceptDeclaration must be identified in context of MapKeyType: NotIdentified/); + // }); + + // it('should throw when an enum key declaration missing', function() { + // (() => { + // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.enum.cto', MapDeclaration); + // decl.validate(); + // }).should.throw(/MapKeyType has invalid Type: NotDeclared/); + // }); + + // it('should throw when map value is an illegal type', function() { + // (() => { + // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badvalue.nontype.cto', MapDeclaration); + // decl.validate(); + // }).should.throw(/MapPropertyType has invalid Type: NONTYPE/); + // }); + it('should throw when map key is a boolean', function() { (() => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.scalar.boolean.cto', MapDeclaration); @@ -270,36 +273,36 @@ describe('MapDeclaration', () => { (() => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.event.cto', MapDeclaration); decl.validate(); - }).should.throw(/MapKeyType has invalid Type: Activity/); + }).should.throw(/Concept as Map Type Key is not supported: Activity/); }); it('should throw when map key is of type MapDeclaration', function() { (() => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.map.cto', MapDeclaration); decl.validate(); - }).should.throw(/MapKeyType has invalid Type: IllegalMapKey/); - }); - - it('should throw when map key is of primitive type Double', function() { - (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.double.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapKeyType has invalid Type: Double/); - }); - - it('should throw when map key is of primitive type Integer', function() { - (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.integer.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapKeyType has invalid Type: Integer/); - }); - - it('should throw when map key is of primitive type Long', function() { - (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.long.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapKeyType has invalid Type: Long/); - }); + }).should.throw(/MapDeclaration as Map Type Value is not supported/); + }); + + // it.only('should throw when map key is of primitive type Double', function() { + // (() => { + // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.double.cto', MapDeclaration); + // decl.validate(); + // }).should.throw(/MapKeyType has invalid Type: Double/); + // }); + + // it('should throw when map key is of primitive type Integer', function() { + // (() => { + // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.integer.cto', MapDeclaration); + // decl.validate(); + // }).should.throw(/MapKeyType has invalid Type: Integer/); + // }); + + // it('should throw when map key is of primitive type Long', function() { + // (() => { + // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.long.cto', MapDeclaration); + // decl.validate(); + // }).should.throw(/MapKeyType has invalid Type: Long/); + // }); it('should throw when map key is of scalar type Double', function() { (() => { @@ -326,17 +329,14 @@ describe('MapDeclaration', () => { describe('#accept', () => { it('should call the visitor', () => { let clz = new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - name: 'String' - }, - { - '$class': 'concerto.metamodel@1.0.0.AggregateValueType', - name: 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } }); let visitor = { visit: sinon.stub() @@ -351,36 +351,45 @@ describe('MapDeclaration', () => { describe('#getKey', () => { it('should return the map key property', () => { let clz = new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - name: 'DateTime' - }, - { - '$class': 'concerto.metamodel@1.0.0.AggregateValueType', - name: 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } }); (clz.getKey() instanceof MapKeyType).should.be.equal(true); - clz.getKey().ast.$class.should.equal('concerto.metamodel@1.0.0.MapKeyType'); - clz.getKey().ast.name.should.equal('DateTime'); + clz.getKey().ast.$class.should.equal('concerto.metamodel@1.0.0.StringMapKeyType'); }); + + // TODO :: Add coverage for Object Scalars it('should return the correct values when called', () => { let clz = new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - name: 'DateTime' - }, - { - '$class': 'concerto.metamodel@1.0.0.AggregateValueType', - name: 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + clz.getKey().getType().should.equal('String'); + }); + + it('should return the correct values when called', () => { + let clz = new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.DateTimeMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } }); clz.getKey().getType().should.equal('DateTime'); }); @@ -389,75 +398,115 @@ describe('MapDeclaration', () => { describe('#getValue', () => { it('should return the map value property', () => { let clz = new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - name: 'DateTime' - }, - { - '$class': 'concerto.metamodel@1.0.0.AggregateValueType', - name: 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } }); (clz.getValue() instanceof MapValueType).should.be.equal(true); - clz.getValue().ast.$class.should.equal('concerto.metamodel@1.0.0.AggregateValueType'); - clz.getValue().ast.name.should.equal('String'); + clz.getValue().ast.$class.should.equal('concerto.metamodel@1.0.0.StringMapValueType'); }); - it('should return the correct values when called', () => { + it('should return the correct values when called (Boolean)', () => { let clz = new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - name: 'DateTime' - }, - { - '$class': 'concerto.metamodel@1.0.0.AggregateValueType', - name: 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.BooleanMapValueType' + } + }); + clz.getValue().getType().should.equal('Boolean'); + }); + + it('should return the correct values when called (DateTime)', () => { + let clz = new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.DateTimeMapValueType' + } + }); + clz.getValue().getType().should.equal('DateTime'); + }); + + it('should return the correct values when called (String)', () => { + let clz = new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } }); clz.getValue().getType().should.equal('String'); }); - }); - describe('#getProperties', () => { - it('should return the map properties', () => { + it('should return the correct values when called (Integer)', () => { let clz = new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - 'name': 'String' - }, - { - '$class': 'concerto.metamodel@1.0.0.AggregateValueType', - 'name': 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.IntegerMapValueType' + } }); - clz.getProperties().length.should.be.equal(2); + clz.getValue().getType().should.equal('Integer'); }); - }); + it('should return the correct values when called (Long)', () => { + let clz = new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.LongMapValueType' + } + }); + clz.getValue().getType().should.equal('Long'); + }); + + it('should return the correct values when called (Double)', () => { + let clz = new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.DoubleMapValueType' + } + }); + clz.getValue().getType().should.equal('Double'); + }); + }); describe('#Introspect', () => { it('should return the correct model file', () => { let clz = new MapDeclaration(modelFile, { - name: 'MapTest', - properties: [ - { - '$class': 'concerto.metamodel@1.0.0.MapKeyType', - 'name': 'String' - }, - { - '$class': 'concerto.metamodel@1.0.0.AggregateValueType', - 'name': 'String' - } - ] + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.DoubleMapValueType' + } }); clz.getModelFile().should.equal(modelFile); clz.getKey().getModelFile().should.equal(modelFile); From 42668f9e9b2627a10dfda5f4ea9749a2ed4c04c7 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Wed, 9 Aug 2023 14:21:24 +0100 Subject: [PATCH 19/41] feat(map): add more test coverage Signed-off-by: jonathan.casey --- .../lib/introspect/mapdeclaration.js | 18 +- .../lib/introspect/mapvaluetype.js | 16 + ...badkey.identified.declaration.concept.cto} | 0 ...odvalue.identified.declaration.concept.cto | 22 ++ ...eclaration.goodvalue.primitive.boolean.cto | 20 ++ .../test/introspect/mapdeclaration.js | 313 +++++++++++------- 6 files changed, 265 insertions(+), 124 deletions(-) rename packages/concerto-core/test/data/parser/mapdeclaration/{mapdeclaration.goodkey.declaration.concept.cto => mapdeclaration.badkey.identified.declaration.concept.cto} (100%) create mode 100644 packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.identified.declaration.concept.cto create mode 100644 packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.boolean.cto diff --git a/packages/concerto-core/lib/introspect/mapdeclaration.js b/packages/concerto-core/lib/introspect/mapdeclaration.js index 915fa24c3..bbc339457 100644 --- a/packages/concerto-core/lib/introspect/mapdeclaration.js +++ b/packages/concerto-core/lib/introspect/mapdeclaration.js @@ -169,12 +169,12 @@ class MapDeclaration extends Declaration { return true; } - // TODO MAKE PRIVATE /** - * Returns true if this class is the definition of a class declaration. - * @param {MapDeclaration} key - the Key for the Map Declaration - * @return {boolean} true if the class is a class - */ + * Returns true if this Key is a valid Map Key. + * + * @param {Object} key - the Key of the Map Declaration + * @return {boolean} true if the Key is a valid Map Key + */ isValidMapKey(key) { return [ `${MetaModelNamespace}.StringMapKeyType`, @@ -183,11 +183,11 @@ class MapDeclaration extends Declaration { ].includes(key.$class); } - // TODO MAKE PRIVATE /** - * Returns true if this class is the definition of a class declaration. - * @param {MapDeclaration} value - the Key for the Map Declaration - * @return {boolean} true if the class is a class + * 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 */ isValidMapValue(value) { return [ diff --git a/packages/concerto-core/lib/introspect/mapvaluetype.js b/packages/concerto-core/lib/introspect/mapvaluetype.js index db0b72a11..33cb9c9aa 100644 --- a/packages/concerto-core/lib/introspect/mapvaluetype.js +++ b/packages/concerto-core/lib/introspect/mapvaluetype.js @@ -90,6 +90,22 @@ class MapValueType extends Decorated { switch(this.ast.$class) { case `${MetaModelNamespace}.ObjectMapValueType`: decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type.name); + + // ObjectMapValueType must have TypeIdentifier. + if (!('type' in ast)) { + throw new IllegalModelException(`ObjectMapValueType must contain property 'type', for MapDeclaration named ${this.parent.name}`); + } + + // ObjectMapValueType TypeIdentifier must be properly formed. + if (!('$class' in ast.type) || !('name' in ast.type)) { + throw new IllegalModelException(`ObjectMapValueType type must contain property '$class' and property 'name', for MapDeclaration named ${this.parent.name}`); + } + + // And the $class must be valid. + if (ast.type.$class !== 'concerto.metamodel@1.0.0.TypeIdentifier') { + throw new IllegalModelException(`ObjectMapValueType type $class must be of TypeIdentifier for MapDeclaration named ${this.parent.name}`); + } + this.type = decl.getName(); break; // case 'RelationshipMapValueType': diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.identified.declaration.concept.cto similarity index 100% rename from packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto rename to packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.identified.declaration.concept.cto diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.identified.declaration.concept.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.identified.declaration.concept.cto new file mode 100644 index 000000000..177158799 --- /dev/null +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.identified.declaration.concept.cto @@ -0,0 +1,22 @@ +/* + * 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 + +concept Person identified {} + +map Dictionary { + o String + o Person +} diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.boolean.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.boolean.cto new file mode 100644 index 000000000..d4eb2ef66 --- /dev/null +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.boolean.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.acme@1.0.0 + +map Dictionary { + o String + o Boolean +} diff --git a/packages/concerto-core/test/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index 900792e83..c5a334e17 100644 --- a/packages/concerto-core/test/introspect/mapdeclaration.js +++ b/packages/concerto-core/test/introspect/mapdeclaration.js @@ -87,11 +87,7 @@ describe('MapDeclaration', () => { process.env.ENABLE_MAP_TYPE = 'true'; // enable after the test run. This is necessary to ensure functioning of other tests. }); - // should throw if ast contains $class for KEY other than - // should throw if ast contains $class for VALUE other than - // should NOT throw if ast contains $class for KEY other than - // should NOT throw if ast contains $class for VALUE other than it('should throw if invalid $class provided for Map Key', () => { (() => @@ -127,202 +123,289 @@ describe('MapDeclaration', () => { }); }); - describe('#validate success scenarios', () => { - - // it('should not throw when map key is an identified concept declaration', () => { - // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto', MapDeclaration); - // decl.validate(); - // }); - - // it('should not throw when map key is an enum declaration', () => { - // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.enum.cto', MapDeclaration); - // decl.validate(); - // }); - - it('should not throw when map key is primitive type datetime', () => { + describe('#validate success scenarios - Map Key', () => { + it('should validate when map key is primitive type datetime', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.primitive.datetime.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map key is primitive type string', () => { + it('should validate when map key is primitive type string', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.primitive.string.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map key is primitive type string datetime', () => { + it('should validate when map key is primitive type scalar datetime', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.scalar.datetime.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map key is primitive type scalar string', () => { + it('should validate when map key is primitive type scalar string', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.scalar.string.cto', MapDeclaration); decl.validate(); }); + }); + - it('should not throw when map value is an identified concept declaration', () => { + describe('#validate success scenarios - Map Value', () => { + it('should validate when map value is a concept declaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.concept.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map value is a concept derived from another concept declaration', () => { + it('should validate when map value is an identified concept declaration', () => { + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.identified.declaration.concept.cto', MapDeclaration); + decl.validate(); + }); + + it('should validate when map value is a concept derived from another concept declaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.derived.concept.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map value is an event declaration', () => { + it('should validate when map value is an event declaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.event.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map value is an asset declaration', () => { + it('should validate when map value is an asset declaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.asset.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map value is an transaction declaration', () => { + it('should validate when map value is an transaction declaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.transaction.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map value is an participant declaration', () => { + it('should validate when map value is an participant declaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.participant.cto', MapDeclaration); decl.validate(); }); + it('should validate when map value is a primitive boolean', () => { + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.boolean.cto', MapDeclaration); + decl.validate(); + }); - it('should not throw when map value is a primitive string', () => { + it('should validate when map value is a primitive string', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.string.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map value is a primitive datetime', () => { + it('should validate when map value is a primitive datetime', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.datetime.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map value is a primitive double', () => { + it('should validate when map value is a primitive double', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.double.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map value is a primitive integer', () => { + it('should validate when map value is a primitive integer', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.integer.cto', MapDeclaration); decl.validate(); }); - it('should not throw when map value is a primitive long', () => { + it('should validate when map value is a primitive long', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.primitive.long.cto', MapDeclaration); decl.validate(); }); - }); - describe('#validate failure scenarios', () => { + describe('#validate failure scenarios - Map Key', () => { - it('should throw when map key is a concept declaration', function() { + it('should throw if ast contains illegal Map Key Property', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.concept.cto', MapDeclaration); - decl.validate(); - }).should.throw(/Concept as Map Type Key is not supported:/); + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.UnSupportedMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + }).should.throw(IllegalModelException); }); - it('should throw when map value is a relationship', function() { + it('should throw if ast contains illegal Map Key Property - Boolean', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badvalue.declaration.relationship.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapDeclaration must contain valid MapValueType, for MapDeclaration Dictionary/); + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.BooleanMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + }).should.throw(IllegalModelException); }); - it('should throw when map value is a map declaration', function() { + it('should throw if ast contains illegal Map Key Property - Integer', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badvalue.declaration.map.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapDeclaration as Map Type Value is not supported:/); + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.IntegerMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + }).should.throw(IllegalModelException); }); + it('should throw if ast contains illegal Map Key Property - Long', () => { + (() => { + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.LongMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + }).should.throw(IllegalModelException); + }); - // it('should throw validating with a non-identified concept declaration as key', function() { - // (() => { - // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.concept.cto', MapDeclaration); - // decl.validate(); - // }).should.throw(/ConceptDeclaration must be identified in context of MapKeyType: NotIdentified/); - // }); + it('should throw if ast contains illegal Map Key Property - Double', () => { + (() => { + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.DoubleMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + }).should.throw(IllegalModelException); + }); - // it('should throw when an enum key declaration missing', function() { - // (() => { - // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.enum.cto', MapDeclaration); - // decl.validate(); - // }).should.throw(/MapKeyType has invalid Type: NotDeclared/); - // }); + it.skip('should throw if ast contains illegal Map Key Property - Concept', () => { + (() => { + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.ObjectMapKeyType', + type: { + $class: 'concerto.metamodel@1.0.0.TypeIdentifier', + name: 'Person' + } + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + }).should.throw(IllegalModelException); + }); - // it('should throw when map value is an illegal type', function() { - // (() => { - // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badvalue.nontype.cto', MapDeclaration); - // decl.validate(); - // }).should.throw(/MapPropertyType has invalid Type: NONTYPE/); - // }); - it('should throw when map key is a boolean', function() { + it('should throw if ast contains illegal Map Key Property - Enum', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.scalar.boolean.cto', MapDeclaration); - decl.validate(); - }).should.throw(/Scalar must be one of StringScalar, DateTimeScalar in context of MapKeyType. Invalid Scalar: BOOL/); + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.EnumMapKeyType', + type: { + $class: 'concerto.metamodel@1.0.0.TypeIdentifier', + name: 'States' + } + }, + + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + }).should.throw(IllegalModelException); }); + }); - it('should throw when map key is an event declaration', function() { + describe('#validate failure scenarios - Map Value', () => { + + it('should throw if ObjectMapValueType does not contain type property ', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.event.cto', MapDeclaration); - decl.validate(); - }).should.throw(/Concept as Map Type Key is not supported: Activity/); + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.ObjectMapValueType', + } + }); + }).should.throw(IllegalModelException); }); - it('should throw when map key is of type MapDeclaration', function() { + it('should throw if ObjectMapValueType TypeIdentifier does not contain name property', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.map.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapDeclaration as Map Type Value is not supported/); - }); - - // it.only('should throw when map key is of primitive type Double', function() { - // (() => { - // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.double.cto', MapDeclaration); - // decl.validate(); - // }).should.throw(/MapKeyType has invalid Type: Double/); - // }); - - // it('should throw when map key is of primitive type Integer', function() { - // (() => { - // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.integer.cto', MapDeclaration); - // decl.validate(); - // }).should.throw(/MapKeyType has invalid Type: Integer/); - // }); - - // it('should throw when map key is of primitive type Long', function() { - // (() => { - // let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.long.cto', MapDeclaration); - // decl.validate(); - // }).should.throw(/MapKeyType has invalid Type: Long/); - // }); - - it('should throw when map key is of scalar type Double', function() { + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.ObjectMapValueType', + type: { + $class: 'concerto.metamodel@1.0.0.TypeIdentifier', + } + } + }); + }).should.throw(IllegalModelException); + }); + + it('should throw if ObjectMapValueType TypeIdentifier has bad $class', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.scalar.double.cto', MapDeclaration); - decl.validate(); - }).should.throw(/Scalar must be one of StringScalar, DateTimeScalar in context of MapKeyType. Invalid Scalar: BAD/); + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.ObjectMapValueType', + type: { + $class: 'concerto.metamodel@1.0.0.BAD_$CLASS', + name: 'Person', + } + } + }); + }).should.throw(IllegalModelException); }); - it('should throw when map key is of scalar type Integer', function() { + it('should throw if ast contains illegal Map Value Property', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.scalar.integer.cto', MapDeclaration); - decl.validate(); - }).should.throw(/Scalar must be one of StringScalar, DateTimeScalar in context of MapKeyType. Invalid Scalar: BAD/); + new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.EnumMapValueType' + } + }); + }).should.throw(IllegalModelException); }); - it('should throw when map key is of scalar type Long', function() { + + it('should throw when map value is a map declaration', function() { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.scalar.long.cto', MapDeclaration); + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badvalue.declaration.map.cto', MapDeclaration); decl.validate(); - }).should.throw(/Scalar must be one of StringScalar, DateTimeScalar in context of MapKeyType. Invalid Scalar: BAD/); + }).should.throw(/MapDeclaration as Map Type Value is not supported:/); }); }); @@ -514,25 +597,25 @@ describe('MapDeclaration', () => { }); it('should return the correct value on introspection', () => { - let declaration = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto', MapDeclaration); + let declaration = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.primitive.string.cto', MapDeclaration); declaration.declarationKind().should.equal('MapDeclaration'); - declaration.getFullyQualifiedName().should.equal('com.testing@1.0.0.Dictionary'); + declaration.getFullyQualifiedName().should.equal('com.acme@1.0.0.Dictionary'); declaration.isMapDeclaration().should.equal(true); }); }); describe('#toString', () => { it('should give the correct value for Map Declaration', () => { - let declaration = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto', MapDeclaration); - declaration.toString().should.equal('MapDeclaration {id=com.testing@1.0.0.Dictionary}'); - declaration.getKey().toString().should.equal('MapKeyType {id=Person}'); + let declaration = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.primitive.string.cto', MapDeclaration); + declaration.toString().should.equal('MapDeclaration {id=com.acme@1.0.0.Dictionary}'); + declaration.getKey().toString().should.equal('MapKeyType {id=String}'); declaration.getValue().toString().should.equal('MapValueType {id=String}'); }); }); describe('#getParent', () => { it('should return the correct parent MapDeclaration value ', () => { - let declaration = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.declaration.concept.cto', MapDeclaration); + let declaration = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.primitive.string.cto', MapDeclaration); declaration.getKey().getParent().should.equal(declaration); declaration.getValue().getParent().should.equal(declaration); }); From 5f627f866e13b80f2efdf71aabbb42f2c3a0b643 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Wed, 9 Aug 2023 15:01:21 +0100 Subject: [PATCH 20/41] feat(map): improve error messaging & cleanup Signed-off-by: jonathan.casey --- .../lib/introspect/mapkeytype.js | 9 +++----- .../lib/introspect/mapvaluetype.js | 4 ---- .../test/introspect/mapdeclaration.js | 21 ------------------- 3 files changed, 3 insertions(+), 31 deletions(-) diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index 1d260ac1a..4dbb49047 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -69,8 +69,6 @@ class MapKeyType extends Decorated { */ validate() { - - // TODO Test cases - illegal Primitives. if (!ModelUtil.isPrimitiveType(this.type)) { let decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type?.name); @@ -78,22 +76,21 @@ class MapKeyType extends Decorated { !(decl?.ast.$class === `${MetaModelNamespace}.StringScalar`) && !(decl?.ast.$class === `${MetaModelNamespace}.DateTimeScalar`)) { throw new IllegalModelException( - `Scalar must be one of StringScalar, DateTimeScalar in context of MapKeyType. Invalid Scalar: ${this.type}` + `Scalar must be one of StringScalar, DateTimeScalar in context of MapKeyType. Invalid Scalar: ${this.type}, for MapDeclaration ${this.parent.name}` ); } if(decl.isMapDeclaration?.()) { throw new IllegalModelException( - `MapDeclaration as Map Type Value is not supported: ${this.type}` + `MapDeclaration as MapKeyType is not supported, for MapDeclaration ${this.parent.name}` ); } if (decl?.isConcept?.() || decl?.isClassDeclaration?.()) { throw new IllegalModelException( - `Concept as Map Type Key is not supported: ${this.type}` + `MapKeyType supports String and DateTime only,for MapDeclaration ${this.parent.name}` ); } - } } diff --git a/packages/concerto-core/lib/introspect/mapvaluetype.js b/packages/concerto-core/lib/introspect/mapvaluetype.js index 33cb9c9aa..951a2b46d 100644 --- a/packages/concerto-core/lib/introspect/mapvaluetype.js +++ b/packages/concerto-core/lib/introspect/mapvaluetype.js @@ -108,10 +108,6 @@ class MapValueType extends Decorated { this.type = decl.getName(); break; - // case 'RelationshipMapValueType': - // // todo - how to handle relationship?? - // this.type = 'Boolean'; - // break; case `${MetaModelNamespace}.BooleanMapValueType`: this.type = 'Boolean'; break; diff --git a/packages/concerto-core/test/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index c5a334e17..aec37015a 100644 --- a/packages/concerto-core/test/introspect/mapdeclaration.js +++ b/packages/concerto-core/test/introspect/mapdeclaration.js @@ -145,7 +145,6 @@ describe('MapDeclaration', () => { }); }); - describe('#validate success scenarios - Map Value', () => { it('should validate when map value is a concept declaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.declaration.concept.cto', MapDeclaration); @@ -290,26 +289,6 @@ describe('MapDeclaration', () => { }).should.throw(IllegalModelException); }); - it.skip('should throw if ast contains illegal Map Key Property - Concept', () => { - (() => { - new MapDeclaration(modelFile, { - $class: 'concerto.metamodel@1.0.0.MapDeclaration', - name: 'MapPermutation1', - key: { - $class: 'concerto.metamodel@1.0.0.ObjectMapKeyType', - type: { - $class: 'concerto.metamodel@1.0.0.TypeIdentifier', - name: 'Person' - } - }, - value: { - $class: 'concerto.metamodel@1.0.0.StringMapValueType' - } - }); - }).should.throw(IllegalModelException); - }); - - it('should throw if ast contains illegal Map Key Property - Enum', () => { (() => { new MapDeclaration(modelFile, { From b5d6c8334e28c302bb8da3dc254165362ee63784 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Wed, 9 Aug 2023 15:11:35 +0100 Subject: [PATCH 21/41] feat(map): adds more test coverage for MapKeyType introspection Signed-off-by: jonathan.casey --- .../test/introspect/mapdeclaration.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/concerto-core/test/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index aec37015a..bb12dd7b6 100644 --- a/packages/concerto-core/test/introspect/mapdeclaration.js +++ b/packages/concerto-core/test/introspect/mapdeclaration.js @@ -301,7 +301,6 @@ describe('MapDeclaration', () => { name: 'States' } }, - value: { $class: 'concerto.metamodel@1.0.0.StringMapValueType' } @@ -426,9 +425,7 @@ describe('MapDeclaration', () => { clz.getKey().ast.$class.should.equal('concerto.metamodel@1.0.0.StringMapKeyType'); }); - - // TODO :: Add coverage for Object Scalars - it('should return the correct values when called', () => { + it('should return the correct values when called - String', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -442,7 +439,7 @@ describe('MapDeclaration', () => { clz.getKey().getType().should.equal('String'); }); - it('should return the correct values when called', () => { + it('should return the correct values when called - DateTime', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -455,6 +452,17 @@ describe('MapDeclaration', () => { }); clz.getKey().getType().should.equal('DateTime'); }); + + + it('should return the correct values when called - Scalar DateTime', () => { + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.scalar.datetime.cto', MapDeclaration); + decl.getKey().getType().should.equal('DATE'); + }); + + it('should return the correct values when called - Scalar String', () => { + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.scalar.string.cto', MapDeclaration); + decl.getKey().getType().should.equal('GUID'); + }); }); describe('#getValue', () => { From 882bf5a1d2d558a9d6ca48edcc272c5aa6164953 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Wed, 9 Aug 2023 15:15:48 +0100 Subject: [PATCH 22/41] feat(map): adds more test coverage for MapValueType introspection Signed-off-by: jonathan.casey --- .../concerto-core/test/introspect/mapdeclaration.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/concerto-core/test/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index bb12dd7b6..966ca2186 100644 --- a/packages/concerto-core/test/introspect/mapdeclaration.js +++ b/packages/concerto-core/test/introspect/mapdeclaration.js @@ -564,6 +564,16 @@ describe('MapDeclaration', () => { }); clz.getValue().getType().should.equal('Double'); }); + + it('should return the correct values when called - Scalar DateTime', () => { + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.datetime.cto', MapDeclaration); + decl.getValue().getType().should.equal('DATE'); + }); + + it('should return the correct values when called - Scalar String', () => { + let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.string.cto', MapDeclaration); + decl.getValue().getType().should.equal('GUID'); + }); }); describe('#Introspect', () => { From 85f8f107cc9bb3355a118a66de3427e1d9b3d820 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 10 Aug 2023 16:29:47 +0100 Subject: [PATCH 23/41] feat(map): refactor serialization Signed-off-by: jonathan.casey --- .../concerto-core/lib/serializer/jsongenerator.js | 5 +++++ .../concerto-core/lib/serializer/jsonpopulator.js | 11 +---------- .../concerto-core/lib/serializer/resourcevalidator.js | 9 +++------ 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/concerto-core/lib/serializer/jsongenerator.js b/packages/concerto-core/lib/serializer/jsongenerator.js index 0445cbf78..65584b581 100644 --- a/packages/concerto-core/lib/serializer/jsongenerator.js +++ b/packages/concerto-core/lib/serializer/jsongenerator.js @@ -91,6 +91,11 @@ class JSONGenerator { obj.forEach((value, key) => { + // don't serialize System Properties, other than $class + if(ModelUtil.isSystemProperty(key) && key !== '$class') { + return; + } + if (typeof key === 'object') { let decl = mapDeclaration.getModelFile() .getAllDeclarations() diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index e7e681899..2c4987465 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -187,39 +187,30 @@ class JSONPopulator { return; } - // If its a Non-Primitive, its likely a ClassDeclaration which needs visiting. if (!ModelUtil.isSystemProperty(key) && !ModelUtil.isPrimitiveType(mapDeclaration.getKey().getType())) { - // get the Key declaration. let decl = mapDeclaration.getModelFile() .getAllDeclarations() .find(decl => decl.name === mapDeclaration.getKey().getType()); - - // parse the Object, and visit the declaration. if (decl?.isClassDeclaration()) { let subResource = parameters.factory.newConcept(decl.getNamespace(), decl.getName(), decl.getIdentifierFieldName() ); - parameters.jsonStack.push(JSON.parse(key)); parameters.resourceStack.push(subResource); - key = decl.accept(this, parameters); } } if (!ModelUtil.isPrimitiveType(mapDeclaration.getValue().getType())) { - // get the Value declaration. let decl = mapDeclaration.getModelFile() .getAllDeclarations() .find(decl => decl.name === mapDeclaration.getValue().getType()); - // parse the Object, and visit the declaration. - if (decl?.isClassDeclaration() ) { + if (decl?.isClassDeclaration()) { let subResource = parameters.factory.newConcept(decl.getNamespace(), decl.getName(), decl.getIdentifierFieldName() ); parameters.jsonStack.push(JSON.parse(value)); parameters.resourceStack.push(subResource); - value = decl.accept(this, parameters); } } diff --git a/packages/concerto-core/lib/serializer/resourcevalidator.js b/packages/concerto-core/lib/serializer/resourcevalidator.js index 28372714a..3df7c4224 100644 --- a/packages/concerto-core/lib/serializer/resourcevalidator.js +++ b/packages/concerto-core/lib/serializer/resourcevalidator.js @@ -128,17 +128,12 @@ class ResourceValidator { .getAllDeclarations() .find(decl => decl.name === type.getType()); - // a ClassDeclaration used in the context of a Map Key must be identified. - if (type.isKey() && thing.isClassDeclaration() && !thing.isIdentified()) { - throw new Error('Map Key must be an Identified Class Declaration'); - } - // if Key or Value is Scalar, get the Base Type of the Scalar for primitive validation. if (ModelUtil.isScalar(mapDeclaration.getKey())) { type = thing.getType(); } - if (thing.isClassDeclaration()) { + if (thing?.isClassDeclaration?.()) { parameters.stack.push(value); thing.accept(this, parameters); return; @@ -197,7 +192,9 @@ class ResourceValidator { obj.forEach((value, key) => { if (!ModelUtil.isSystemProperty(key)) { + // Validate Key this.checkMapType(mapDeclaration.getKey(), key, parameters, mapDeclaration); + // Validate Value this.checkMapType(mapDeclaration.getValue(), value, parameters, mapDeclaration); } }); From 2f8d622ccd16a9c455094a6c98b7dac2cd89a13f Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 10 Aug 2023 16:30:17 +0100 Subject: [PATCH 24/41] feat(map): refactor serialization test coverage Signed-off-by: jonathan.casey --- ...mapdeclaration.badkey.declaration.enum.cto | 7 +- ...pdeclaration.goodvalue.scalar.datetime.cto | 22 + ...mapdeclaration.goodvalue.scalar.string.cto | 22 + .../test/serializer/maptype/serializer.js | 1134 ++++++++++------- 4 files changed, 711 insertions(+), 474 deletions(-) create mode 100644 packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.datetime.cto create mode 100644 packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.string.cto diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.enum.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.enum.cto index 3989eb351..cfb5c3ca1 100644 --- a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.enum.cto +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.enum.cto @@ -13,7 +13,12 @@ */ namespace com.testing@1.0.0 +enum Phase { + o ONE + o TWO +} map Dictionary { - o NotDeclared + o Phase o String } + diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.datetime.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.datetime.cto new file mode 100644 index 000000000..f2ccd0202 --- /dev/null +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.datetime.cto @@ -0,0 +1,22 @@ +/* + * 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.acme@1.0.0 + +scalar DATE extends DateTime + +map Dictionary { + o String + o DATE +} diff --git a/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.string.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.string.cto new file mode 100644 index 000000000..361875814 --- /dev/null +++ b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.string.cto @@ -0,0 +1,22 @@ +/* + * 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.acme@1.0.0 + +scalar GUID extends String + +map Dictionary { + o String + o GUID +} diff --git a/packages/concerto-core/test/serializer/maptype/serializer.js b/packages/concerto-core/test/serializer/maptype/serializer.js index 4de307ea1..ad4866d14 100644 --- a/packages/concerto-core/test/serializer/maptype/serializer.js +++ b/packages/concerto-core/test/serializer/maptype/serializer.js @@ -19,7 +19,7 @@ const ModelManager = require('../../../lib/modelmanager'); const Resource = require('../../../lib/model/resource'); const Serializer = require('../../../lib/serializer'); const Util = require('../../composer/composermodelutility'); -const ModelUtil = require('../../../../concerto-core/lib/modelutil'); +// const ModelUtil = require('../../../../concerto-core/lib/modelutil'); require('chai').should(); const sinon = require('sinon'); @@ -32,14 +32,17 @@ describe('Serializer', () => { let serializer; beforeEach(() => { + process.env.ENABLE_MAP_TYPE = 'true'; + + sandbox = sinon.createSandbox(); modelManager = new ModelManager(); Util.addComposerModel(modelManager); + modelManager.addCTOModel(` namespace org.acme.sample - concept Concepts { o Dictionary dict optional o Diary diary optional @@ -48,31 +51,26 @@ describe('Serializer', () => { o RSVP rsvp optional o Database database optional o Appointment appointment optional - o MarriageRegister marriages optional - o PostCodeEntries postcodes optional - o AddressBook addressBook optional o Birthday birthday optional o Celebration celebration optional - o ExaminationGrade grade optional o Rolodex rolodex optional - o Attendance attendance optional - o Vegan vegan optional - o Reservation reservation optional - o GuestList vip optional - o Graduated graduated optional o Directory directory optional - o Meeting meeting optional - o Graduation graduation optional - o Team team optional - o StateLog stateLog optional + o Score score optional + o Points points optional + o Balance balance optional } - map Dictionary { - o String - o String + scalar GUID extends String + + scalar Time extends DateTime + + scalar PostalCode extends String + + concept Person identified by name { + o String name } - map PhoneBook { + map Dictionary { o String o String } @@ -97,26 +95,11 @@ describe('Serializer', () => { o Boolean } - map Attendance { - o DateTime - o Boolean - } - - map Meeting { - o Time - o Person - } - map Database { o GUID o String } - map Vegan { - o GUID - o Boolean - } - map Directory { o GUID o Person @@ -137,82 +120,29 @@ describe('Serializer', () => { o Time } - map Graduation { - o DateTime - o Person - } - map Rolodex { o String o Person } - map MarriageRegister { - o Person - o Person - } - - map ExaminationGrade { - o Person + map Score { o String + o Integer } - map Reservation { - o Person - o Time - } - - map Graduated { - o Person - o DateTime - } - - map GuestList { - o Person - o Boolean - } - - map PostCodeEntries { - o GUID - o PostalCode - } - - map AddressBook { - o GUID - --> Person - } - - map Team { - o Person - o Leader - } - - scalar GUID extends String - - scalar Time extends DateTime - - scalar PostalCode extends String - - concept Person identified by name { - o String name - } - - concept Leader { - o String name - } - - enum State { - o ON - o OFF + map Points { + o String + o Long } - map StateLog { - o State - o DateTime + map Balance { + o String + o Double } `); factory = new Factory(modelManager); + serializer = new Serializer(factory, modelManager); }); @@ -220,9 +150,9 @@ describe('Serializer', () => { sandbox.restore(); }); - describe('#toJSON', () => { - - it('should generate a JSON object with a Map ', () => { + describe('# toJSON <> fromJSON', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); concept.dict = new Map(); @@ -230,6 +160,7 @@ describe('Serializer', () => { concept.dict.set('Lorem', 'Ipsum'); concept.dict.set('Ipsum', 'Lorem'); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ @@ -240,212 +171,336 @@ describe('Serializer', () => { Ipsum: 'Lorem' } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.dict.should.be.an.instanceOf(Map); + resource.dict.get('$class').should.equal('org.acme.sample.Dictionary'); + resource.dict.get('Lorem').should.equal('Ipsum'); + resource.dict.get('Ipsum').should.equal('Lorem'); }); - it('should generate a JSON object with a Map ', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - concept.rsvp = new Map(); - concept.rsvp.set('$class', 'org.acme.sample.RSVP'); - concept.rsvp.set('Lorem', true); - concept.rsvp.set('Ipsum', false); + concept.score = new Map(); + concept.score.set('$class', 'org.acme.sample.Score'); + concept.score.set('Bob', 1); + concept.score.set('Alice', 1); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - rsvp: { - $class: 'org.acme.sample.RSVP', - 'Lorem': true, - 'Ipsum': false, + score: { + $class: 'org.acme.sample.Score', + Bob: 1, + Alice: 1 } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.score.should.be.an.instanceOf(Map); + resource.score.get('$class').should.equal('org.acme.sample.Score'); + resource.score.get('Bob').should.equal(1); + resource.score.get('Alice').should.equal(1); }); - it('should generate a JSON object with a Map ', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - concept.birthday = new Map(); - concept.birthday.set('$class', 'org.acme.sample.Birthday'); - concept.birthday.set('Lorem', '2023-10-28T01:02:03Z'); - concept.birthday.set('Ipsum', '2023-11-28T01:02:03Z'); + concept.points = new Map(); + concept.points.set('$class', 'org.acme.sample.Points'); + concept.points.set('Bob', -398741129664271); + concept.points.set('Alice', 8999999125356546); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - birthday: { - $class: 'org.acme.sample.Birthday', - 'Lorem': '2023-10-28T01:02:03Z', - 'Ipsum': '2023-11-28T01:02:03Z' + points: { + $class: 'org.acme.sample.Points', + Bob: -398741129664271, + Alice: 8999999125356546 } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.points.should.be.an.instanceOf(Map); + resource.points.get('$class').should.equal('org.acme.sample.Points'); + resource.points.get('Bob').should.equal(-398741129664271); + resource.points.get('Alice').should.equal(8999999125356546); }); - it('should generate a JSON object with a Map ', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); - const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); - - concept.rolodex = new Map(); - concept.rolodex.set('$class', 'org.acme.sample.Rolodex'); - concept.rolodex.set('Abbeyleix', bob); - concept.rolodex.set('Ireland', alice); + concept.balance = new Map(); + concept.balance.set('$class', 'org.acme.sample.Balance'); + concept.balance.set('Bob', 99999.99); + concept.balance.set('Alice', 1000000.00); + // serialize and assert const json = serializer.toJSON(concept); + json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - rolodex: { - $class: 'org.acme.sample.Rolodex', - 'Abbeyleix': '{"$class":"org.acme.sample.Person","name":"Bob"}', - 'Ireland': '{"$class":"org.acme.sample.Person","name":"Alice"}' + balance: { + $class: 'org.acme.sample.Balance', + Bob: 99999.99, + Alice: 1000000.00 } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.balance.should.be.an.instanceOf(Map); + resource.balance.get('$class').should.equal('org.acme.sample.Balance'); + resource.balance.get('Bob').should.equal(99999.99); + resource.balance.get('Alice').should.equal(1000000.00); }); - it('should generate a JSON object with a Map , where Scalar extends String', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - concept.celebration = new Map(); - concept.celebration.set('$class', 'org.acme.sample.Celebration'); - concept.celebration.set('D4F45017-AD2B-416B-AD9F-3B74F7DEA291', '2022-11-28T01:02:03Z'); - concept.celebration.set('9FAE34BF-18C3-4770-A6AA-6F7656C356B8', '2023-10-28T01:02:03Z'); + concept.rsvp = new Map(); + concept.rsvp.set('$class', 'org.acme.sample.RSVP'); + concept.rsvp.set('Bob', true); + concept.rsvp.set('Alice', false); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - celebration: { - $class: 'org.acme.sample.Celebration', - 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': '2022-11-28T01:02:03Z', - '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': '2023-10-28T01:02:03Z', + rsvp: { + $class: 'org.acme.sample.RSVP', + Bob: true, + Alice: false } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.rsvp.should.be.an.instanceOf(Map); + resource.rsvp.get('$class').should.equal('org.acme.sample.RSVP'); + resource.rsvp.get('Bob').should.equal(true); + resource.rsvp.get('Alice').should.equal(false); }); - it('should generate a JSON object with a Map ', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - concept.diary = new Map(); - concept.diary.set('$class', 'org.acme.sample.Diary'); - concept.diary.set('2023-10-28T01:02:03Z', 'Ipsum'); + concept.birthday = new Map(); + concept.birthday.set('$class', 'org.acme.sample.Birthday'); + concept.birthday.set('Bob', '2023-10-28T01:02:03Z'); + concept.birthday.set('Alice', '2024-10-28T01:02:03Z'); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - diary: { - $class: 'org.acme.sample.Diary', - '2023-10-28T01:02:03Z': 'Ipsum', + birthday: { + $class: 'org.acme.sample.Birthday', + Bob: '2023-10-28T01:02:03Z', + Alice: '2024-10-28T01:02:03Z' } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.birthday.should.be.an.instanceOf(Map); + resource.birthday.get('$class').should.equal('org.acme.sample.Birthday'); + resource.birthday.get('Bob').should.equal('2023-10-28T01:02:03Z'); + resource.birthday.get('Alice').should.equal('2024-10-28T01:02:03Z'); }); - it('should generate a JSON object with a Map ', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - concept.attendance = new Map(); - concept.attendance.set('$class', 'org.acme.sample.Attendance'); - concept.attendance.set('2023-10-28T01:02:03Z', true); - concept.attendance.set('2023-11-28T01:02:03Z', false); + concept.celebration = new Map(); + concept.celebration.set('$class', 'org.acme.sample.Celebration'); + concept.celebration.set('BobBirthday', '2022-11-28T01:02:03Z'); + concept.celebration.set('AliceAnniversary', '2023-10-28T01:02:03Z'); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - attendance: { - $class: 'org.acme.sample.Attendance', - '2023-10-28T01:02:03Z': true, - '2023-11-28T01:02:03Z': false, + celebration: { + $class: 'org.acme.sample.Celebration', + 'BobBirthday': '2022-11-28T01:02:03Z', + 'AliceAnniversary': '2023-10-28T01:02:03Z', } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.celebration.should.be.an.instanceOf(Map); + resource.celebration.get('$class').should.equal('org.acme.sample.Celebration'); + resource.celebration.get('BobBirthday').should.equal('2022-11-28T01:02:03Z'); + resource.celebration.get('AliceAnniversary').should.equal('2023-10-28T01:02:03Z'); }); - it('should generate a JSON object with a Map ', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - const person = factory.newConcept('org.acme.sample', 'Person', 'Bob'); + const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); - concept.graduation = new Map(); - concept.graduation.set('$class', 'org.acme.sample.Graduation'); - concept.graduation.set('2023-10-28T01:02:03Z', person); - concept.graduation.set('2023-11-28T01:02:03Z', alice); + concept.rolodex = new Map(); + concept.rolodex.set('$class', 'org.acme.sample.Rolodex'); + concept.rolodex.set('Dublin', bob); + concept.rolodex.set('London', alice); + // serialize & assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - graduation: { - $class: 'org.acme.sample.Graduation', - '2023-10-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Bob"}', - '2023-11-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Alice"}' + rolodex: { + $class: 'org.acme.sample.Rolodex', + 'Dublin': '{"$class":"org.acme.sample.Person","name":"Bob"}', + 'London': '{"$class":"org.acme.sample.Person","name":"Alice"}' } }); + + // deserialize & assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.rolodex.should.be.an.instanceOf(Map); + resource.rolodex.get('$class').should.equal('org.acme.sample.Rolodex'); + + resource.rolodex.get('Dublin').should.be.an.instanceOf(Resource); + resource.rolodex.get('London').should.be.an.instanceOf(Resource); + + resource.rolodex.get('Dublin').toJSON().should.deep.equal({ '$class': 'org.acme.sample.Person', name: 'Bob' }); + resource.rolodex.get('London').toJSON().should.deep.equal({ '$class': 'org.acme.sample.Person', name: 'Alice' }); }); - it('should generate a JSON object with a Map , where Scalar extends String', () => { + it('should serialize -> deserialize with a Map : Scalar extends DateTime', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - concept.stopwatch = new Map(); - concept.stopwatch.set('$class', 'org.acme.sample.StopWatch'); - concept.stopwatch.set('2023-10-28T00:02:03Z', '2023-10-28T01:02:03Z'); - concept.stopwatch.set('2023-11-28T00:02:03Z', '2023-11-28T01:02:03Z'); + concept.appointment = new Map(); + concept.appointment.set('$class', 'org.acme.sample.Appointment'); + concept.appointment.set('2023-11-28T01:02:03Z', 'BobBirthday'); + concept.appointment.set('2024-10-28T01:02:03Z', 'AliceAnniversary'); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - stopwatch: { - $class: 'org.acme.sample.StopWatch', - '2023-10-28T00:02:03Z': '2023-10-28T01:02:03Z', - '2023-11-28T00:02:03Z': '2023-11-28T01:02:03Z', + appointment: { + '$class': 'org.acme.sample.Appointment', + '2023-11-28T01:02:03Z': 'BobBirthday', + '2024-10-28T01:02:03Z': 'AliceAnniversary' } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.appointment.should.be.an.instanceOf(Map); + resource.appointment.get('$class').should.equal('org.acme.sample.Appointment'); + resource.appointment.get('2023-11-28T01:02:03Z').should.equal('BobBirthday'); + resource.appointment.get('2024-10-28T01:02:03Z').should.equal('AliceAnniversary'); }); - it('should generate a JSON object with a Map , where Scalar extends String', () => { + it('should serialize -> deserialize with a Map : Scalar extends String', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); concept.database = new Map(); concept.database.set('$class', 'org.acme.sample.Database'); - concept.database.set('D4F45017-AD2B-416B-AD9F-3B74F7DEA291', 'Lorem'); - concept.database.set('9FAE34BF-18C3-4770-A6AA-6F7656C356B8', 'Ipsum'); + concept.database.set('D4F45017-AD2B-416B-AD9F-3B74F7DEA291', 'Bob'); + concept.database.set('E17B69D9-9B57-4C4A-957E-8B202D7B6C5A', 'Alice'); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', database: { - $class: 'org.acme.sample.Database', - 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': 'Lorem', - '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': 'Ipsum', + '$class': 'org.acme.sample.Database', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': 'Bob', + 'E17B69D9-9B57-4C4A-957E-8B202D7B6C5A': 'Alice' } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.database.should.be.an.instanceOf(Map); + resource.database.get('$class').should.equal('org.acme.sample.Database'); + resource.database.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').should.equal('Bob'); + resource.database.get('E17B69D9-9B57-4C4A-957E-8B202D7B6C5A').should.equal('Alice'); }); - it('should generate a JSON object with a Map , where Scalar extends String', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - concept.vegan = new Map(); - concept.vegan.set('$class', 'org.acme.sample.Vegan'); - concept.vegan.set('D4F45017-AD2B-416B-AD9F-3B74F7DEA291', true); - concept.vegan.set('9FAE34BF-18C3-4770-A6AA-6F7656C356B8', false); + concept.stopwatch = new Map(); + concept.stopwatch.set('$class', 'org.acme.sample.StopWatch'); + concept.stopwatch.set('2023-10-28T00:00:00Z', '2023-10-28T11:12:13Z'); + concept.stopwatch.set('2024-11-28T00:00:00Z', '2024-11-28T11:12:13Z'); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - vegan: { - $class: 'org.acme.sample.Vegan', - 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': true, - '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': false, + stopwatch: { + $class: 'org.acme.sample.StopWatch', + '2023-10-28T00:00:00Z': '2023-10-28T11:12:13Z', + '2024-11-28T00:00:00Z': '2024-11-28T11:12:13Z', } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.stopwatch.should.be.an.instanceOf(Map); + resource.stopwatch.get('$class').should.equal('org.acme.sample.StopWatch'); + resource.stopwatch.get('2023-10-28T00:00:00Z').should.equal('2023-10-28T11:12:13Z'); + resource.stopwatch.get('2024-11-28T00:00:00Z').should.equal('2024-11-28T11:12:13Z'); }); - it('should generate a JSON object with a Map , where Scalar extends String', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); @@ -456,6 +511,7 @@ describe('Serializer', () => { concept.directory.set('D4F45017-AD2B-416B-AD9F-3B74F7DEA291', bob); concept.directory.set('9FAE34BF-18C3-4770-A6AA-6F7656C356B8', alice); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ @@ -466,164 +522,493 @@ describe('Serializer', () => { '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': '{"$class":"org.acme.sample.Person","name":"Alice"}', } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.directory.should.be.an.instanceOf(Map); + + resource.directory.get('$class').should.equal('org.acme.sample.Directory'); + + resource.directory.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').should.be.an.instanceOf(Resource); + resource.directory.get('9FAE34BF-18C3-4770-A6AA-6F7656C356B8').should.be.an.instanceOf(Resource); + + resource.directory.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').toJSON().should.deep.equal({ '$class': 'org.acme.sample.Person', name: 'Bob' }); + resource.directory.get('9FAE34BF-18C3-4770-A6AA-6F7656C356B8').toJSON().should.deep.equal({ '$class': 'org.acme.sample.Person', name: 'Alice' }); }); - it('should generate a JSON object with a Map , where Scalar extends DateTime', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup let concept = factory.newConcept('org.acme.sample', 'Concepts'); - concept.appointment = new Map(); - concept.appointment.set('$class', 'org.acme.sample.Appointment'); - concept.appointment.set('2023-10-28T01:02:03Z', 'Lorem'); - concept.appointment.set('2023-11-28T01:02:03Z', 'Ipsum'); + concept.diary = new Map(); + concept.diary.set('$class', 'org.acme.sample.Diary'); + concept.diary.set('2023-10-28T01:02:03Z', 'Birthday'); + concept.diary.set('2024-10-28T01:02:03Z', 'Anniversary'); + // serialize and assert const json = serializer.toJSON(concept); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - appointment: { - $class: 'org.acme.sample.Appointment', - '2023-10-28T01:02:03Z': 'Lorem', - '2023-11-28T01:02:03Z': 'Ipsum', + diary: { + $class: 'org.acme.sample.Diary', + '2023-10-28T01:02:03Z': 'Birthday', + '2024-10-28T01:02:03Z': 'Anniversary' } }); + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.diary.should.be.an.instanceOf(Map); + resource.diary.get('$class').should.equal('org.acme.sample.Diary'); + resource.diary.get('2023-10-28T01:02:03Z').should.equal('Birthday'); + resource.diary.get('2024-10-28T01:02:03Z').should.equal('Anniversary'); }); + }); - it('should generate a JSON object with a Map , where Scalar extends DateTime', () => { - let concept = factory.newConcept('org.acme.sample', 'Concepts'); + describe('# fromJSON <> toJSON', () => { + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + dict: { + '$class': 'org.acme.sample.Dictionary', + Bob: 'Ipsum', + Alice: 'Lorem' + } + }; - const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); - const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); + // deserialize and assert + let resource = serializer.fromJSON(json); - concept.meeting = new Map(); - concept.meeting.set('$class', 'org.acme.sample.Meeting'); - concept.meeting.set('2023-10-28T01:02:03Z', bob); - concept.meeting.set('2023-11-28T01:02:03Z', alice); + resource.should.be.an.instanceOf(Resource); + resource.dict.should.be.an.instanceOf(Map); + resource.dict.get('$class').should.equal('org.acme.sample.Dictionary'); + resource.dict.get('Bob').should.equal('Ipsum'); + resource.dict.get('Alice').should.equal('Lorem'); - const json = serializer.toJSON(concept); + // serialize and assert + json = serializer.toJSON(resource); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - meeting: { - $class: 'org.acme.sample.Meeting', - '2023-10-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Bob"}', - '2023-11-28T01:02:03Z': '{"$class":"org.acme.sample.Person","name":"Alice"}' + dict: { + $class: 'org.acme.sample.Dictionary', + Bob: 'Ipsum', + Alice: 'Lorem' } }); }); - it('should generate a JSON object with a Map , where Concept is Identified', () => { - let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + score: { + '$class': 'org.acme.sample.Score', + Bob: 1, + Alice: 1 + } + }; - const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); - const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); + // deserialize and assert + let resource = serializer.fromJSON(json); - concepts.grade = new Map(); - concepts.grade.set('$class', 'org.acme.sample.ExaminationGrade'); - concepts.grade.set(bob, 'A+'); - concepts.grade.set(alice, 'B+'); + resource.should.be.an.instanceOf(Resource); + resource.score.should.be.an.instanceOf(Map); + resource.score.get('$class').should.equal('org.acme.sample.Score'); + resource.score.get('Bob').should.equal(1); + resource.score.get('Alice').should.equal(1); - const json = serializer.toJSON(concepts); + // serialize and assert + json = serializer.toJSON(resource); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - grade: { - '$class': 'org.acme.sample.ExaminationGrade', - '{"$class":"org.acme.sample.Person","name":"Bob"}' : 'A+', - '{"$class":"org.acme.sample.Person","name":"Alice"}' : 'B+' + score: { + $class: 'org.acme.sample.Score', + Bob: 1, + Alice: 1 } }); }); - it('should generate a JSON object with a Map , where Concept is Identified & Scalar extends DateTime ', () => { - let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + points: { + '$class': 'org.acme.sample.Points', + Bob: -398741129664271, + Alice: 8999999125356546 + } + }; - const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); - const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); + // deserialize and assert + let resource = serializer.fromJSON(json); - concepts.reservation = new Map(); - concepts.reservation.set('$class', 'org.acme.sample.Reservation'); - concepts.reservation.set(bob, '2023-10-28T01:02:03Z'); - concepts.reservation.set(alice, '2023-11-28T01:02:03Z'); + resource.should.be.an.instanceOf(Resource); + resource.points.should.be.an.instanceOf(Map); + resource.points.get('$class').should.equal('org.acme.sample.Points'); + resource.points.get('Bob').should.equal(-398741129664271); + resource.points.get('Alice').should.equal(8999999125356546); - const json = serializer.toJSON(concepts); + // serialize and assert + json = serializer.toJSON(resource); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - reservation: { - $class: 'org.acme.sample.Reservation', - '{"$class":"org.acme.sample.Person","name":"Bob"}' : '2023-10-28T01:02:03Z', - '{"$class":"org.acme.sample.Person","name":"Alice"}' : '2023-11-28T01:02:03Z' + points: { + $class: 'org.acme.sample.Points', + Bob: -398741129664271, + Alice: 8999999125356546 } }); }); - it('should generate a JSON object with a Map , where Concept is Identified', () => { - let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + balance: { + $class: 'org.acme.sample.Balance', + Bob: 99999.99, + Alice: 1000000.00 + } + }; - const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); - const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); + // deserialize and assert + let resource = serializer.fromJSON(json); - concepts.vip = new Map(); - concepts.vip.set('$class', 'org.acme.sample.GuestList'); - concepts.vip.set(bob, true); - concepts.vip.set(alice, true); + resource.should.be.an.instanceOf(Resource); + resource.balance.should.be.an.instanceOf(Map); + resource.balance.get('$class').should.equal('org.acme.sample.Balance'); + resource.balance.get('Bob').should.equal(99999.99); + resource.balance.get('Alice').should.equal(1000000.00); - const json = serializer.toJSON(concepts); + // serialize and assert + json = serializer.toJSON(resource); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - vip: { - $class: 'org.acme.sample.GuestList', - '{"$class":"org.acme.sample.Person","name":"Bob"}' : true, - '{"$class":"org.acme.sample.Person","name":"Alice"}' : true + balance: { + $class: 'org.acme.sample.Balance', + Bob: 99999.99, + Alice: 1000000.00 } }); }); - it('should generate a JSON object with a Map , where Concept is Identified', () => { - let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + rsvp: { + $class: 'org.acme.sample.RSVP', + Bob: true, + Alice: false + } + }; - const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); - const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); + // deserialize and assert + let resource = serializer.fromJSON(json); - concepts.marriages = new Map(); - concepts.marriages.set('$class', 'org.acme.sample.MarriageRegister'); - concepts.marriages.set(bob, alice); + resource.should.be.an.instanceOf(Resource); + resource.rsvp.should.be.an.instanceOf(Map); + resource.rsvp.get('$class').should.equal('org.acme.sample.RSVP'); + resource.rsvp.get('Bob').should.equal(true); + resource.rsvp.get('Alice').should.equal(false); - const json = serializer.toJSON(concepts); + // serialize and assert + json = serializer.toJSON(resource); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - marriages: { - $class: 'org.acme.sample.MarriageRegister', - '{"$class":"org.acme.sample.Person","name":"Bob"}' : '{"$class":"org.acme.sample.Person","name":"Alice"}' + rsvp: { + $class: 'org.acme.sample.RSVP', + Bob: true, + Alice: false } }); }); - it('should generate a JSON object with a Map , where Concept is Identified', () => { - let concepts = factory.newConcept('org.acme.sample', 'Concepts'); + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + birthday: { + $class: 'org.acme.sample.Birthday', + Bob: '2023-10-28T01:02:03Z', + Alice: '2024-10-28T01:02:03Z' + } + }; - const bob = factory.newConcept('org.acme.sample', 'Person', 'Bob'); - const alice = factory.newConcept('org.acme.sample', 'Person', 'Alice'); + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.birthday.should.be.an.instanceOf(Map); + resource.birthday.get('$class').should.equal('org.acme.sample.Birthday'); + resource.birthday.get('Bob').should.equal('2023-10-28T01:02:03Z'); + resource.birthday.get('Alice').should.equal('2024-10-28T01:02:03Z'); + + // serialize and assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + birthday: { + $class: 'org.acme.sample.Birthday', + Bob: '2023-10-28T01:02:03Z', + Alice: '2024-10-28T01:02:03Z' + } + }); + }); + + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + celebration: { + $class: 'org.acme.sample.Celebration', + 'BobBirthday': '2022-11-28T01:02:03Z', + 'AliceAnniversary': '2023-10-28T01:02:03Z', + } + }; + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.celebration.should.be.an.instanceOf(Map); + resource.celebration.get('$class').should.equal('org.acme.sample.Celebration'); + resource.celebration.get('BobBirthday').should.equal('2022-11-28T01:02:03Z'); + resource.celebration.get('AliceAnniversary').should.equal('2023-10-28T01:02:03Z'); + + // serialize and assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + celebration: { + $class: 'org.acme.sample.Celebration', + 'BobBirthday': '2022-11-28T01:02:03Z', + 'AliceAnniversary': '2023-10-28T01:02:03Z', + } + }); + + }); + + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + rolodex: { + $class: 'org.acme.sample.Rolodex', + 'Dublin': '{"$class":"org.acme.sample.Person","name":"Bob"}', + 'London': '{"$class":"org.acme.sample.Person","name":"Alice"}' + } + }; + + // deserialize & assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.rolodex.should.be.an.instanceOf(Map); + resource.rolodex.get('$class').should.equal('org.acme.sample.Rolodex'); + + resource.rolodex.get('Dublin').should.be.an.instanceOf(Resource); + resource.rolodex.get('London').should.be.an.instanceOf(Resource); + + resource.rolodex.get('Dublin').toJSON().should.deep.equal({ '$class': 'org.acme.sample.Person', name: 'Bob' }); + resource.rolodex.get('London').toJSON().should.deep.equal({ '$class': 'org.acme.sample.Person', name: 'Alice' }); + + // serialize & assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + rolodex: { + $class: 'org.acme.sample.Rolodex', + 'Dublin': '{"$class":"org.acme.sample.Person","name":"Bob"}', + 'London': '{"$class":"org.acme.sample.Person","name":"Alice"}' + } + }); + }); + + it('should deserialize -> serialize with a Map - Scalar extends DateTime', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + appointment: { + '$class': 'org.acme.sample.Appointment', + '2023-11-28T01:02:03Z': 'Lorem', + '2024-10-28T01:02:03Z': 'Ipsum' + } + }; + + // deserialize & assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.appointment.should.be.an.instanceOf(Map); + resource.appointment.get('$class').should.equal('org.acme.sample.Appointment'); + resource.appointment.get('2023-11-28T01:02:03Z').should.equal('Lorem'); + resource.appointment.get('2024-10-28T01:02:03Z').should.equal('Ipsum'); + + // serialize & assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + appointment: { + '$class': 'org.acme.sample.Appointment', + '2023-11-28T01:02:03Z': 'Lorem', + '2024-10-28T01:02:03Z': 'Ipsum' + } + }); + }); + + it('should deserialize -> serialize with a Map - Scalar extends String', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + database: { + '$class': 'org.acme.sample.Database', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': 'Lorem', + 'E17B69D9-9B57-4C4A-957E-8B202D7B6C5A': 'Ipsum' + } + }; - concepts.graduated = new Map(); - concepts.graduated.set('$class', 'org.acme.sample.Graduated'); - concepts.graduated.set(bob, '2023-10-28T01:02:03Z'); - concepts.graduated.set(alice, '2023-11-28T01:02:03Z'); + // deserialize & assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.database.should.be.an.instanceOf(Map); + resource.database.get('$class').should.equal('org.acme.sample.Database'); + resource.database.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').should.equal('Lorem'); + resource.database.get('E17B69D9-9B57-4C4A-957E-8B202D7B6C5A').should.equal('Ipsum'); - const json = serializer.toJSON(concepts); + // serialize & assert + json = serializer.toJSON(resource); json.should.deep.equal({ $class: 'org.acme.sample.Concepts', - graduated: { - $class: 'org.acme.sample.Graduated', - '{"$class":"org.acme.sample.Person","name":"Bob"}' : '2023-10-28T01:02:03Z', - '{"$class":"org.acme.sample.Person","name":"Alice"}' : '2023-11-28T01:02:03Z' + database: { + '$class': 'org.acme.sample.Database', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': 'Lorem', + 'E17B69D9-9B57-4C4A-957E-8B202D7B6C5A': 'Ipsum' } }); }); + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + stopwatch: { + $class: 'org.acme.sample.StopWatch', + '2023-10-28T00:00:00Z': '2023-10-28T11:12:13Z', + '2024-11-28T00:00:00Z': '2024-11-28T11:12:13Z', + } + }; + + // deserialize & assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.stopwatch.should.be.an.instanceOf(Map); + resource.stopwatch.get('$class').should.equal('org.acme.sample.StopWatch'); + resource.stopwatch.get('2023-10-28T00:00:00Z').should.equal('2023-10-28T11:12:13Z'); + resource.stopwatch.get('2024-11-28T00:00:00Z').should.equal('2024-11-28T11:12:13Z'); + + // serialize & assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + stopwatch: { + $class: 'org.acme.sample.StopWatch', + '2023-10-28T00:00:00Z': '2023-10-28T11:12:13Z', + '2024-11-28T00:00:00Z': '2024-11-28T11:12:13Z', + } + }); + }); + + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + directory: { + $class: 'org.acme.sample.Directory', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': '{"$class":"org.acme.sample.Person","name":"Bob"}', + '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': '{"$class":"org.acme.sample.Person","name":"Alice"}', + } + }; + + // deserialize & assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.directory.should.be.an.instanceOf(Map); + resource.directory.get('$class').should.equal('org.acme.sample.Directory'); + resource.directory.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').should.be.an.instanceOf(Resource); + resource.directory.get('9FAE34BF-18C3-4770-A6AA-6F7656C356B8').should.be.an.instanceOf(Resource); + resource.directory.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').toJSON().should.deep.equal({ '$class': 'org.acme.sample.Person', name: 'Bob' }); + resource.directory.get('9FAE34BF-18C3-4770-A6AA-6F7656C356B8').toJSON().should.deep.equal({ '$class': 'org.acme.sample.Person', name: 'Alice' }); + + // serialize & assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + directory: { + $class: 'org.acme.sample.Directory', + 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': '{"$class":"org.acme.sample.Person","name":"Bob"}', + '9FAE34BF-18C3-4770-A6AA-6F7656C356B8': '{"$class":"org.acme.sample.Person","name":"Alice"}', + } + }); + }); + + it('should deserialize -> serialize with a Map ', () => { + // setup + let json = { + $class: 'org.acme.sample.Concepts', + diary: { + $class: 'org.acme.sample.Diary', + '2023-10-28T01:02:03Z': 'Birthday', + '2024-10-28T01:02:03Z': 'Anniversary' + } + }; + + // deserialize and assert + let resource = serializer.fromJSON(json); + + resource.should.be.an.instanceOf(Resource); + resource.diary.should.be.an.instanceOf(Map); + resource.diary.get('$class').should.equal('org.acme.sample.Diary'); + resource.diary.get('2023-10-28T01:02:03Z').should.equal('Birthday'); + resource.diary.get('2024-10-28T01:02:03Z').should.equal('Anniversary'); + + // serialize and assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + diary: { + $class: 'org.acme.sample.Diary', + '2023-10-28T01:02:03Z': 'Birthday', + '2024-10-28T01:02:03Z': 'Anniversary' + } + }); + }); + }); + + describe('#toJSON failure scenarios', () => { it('should throw if bad Key value is provided for Map, where Key Type DateTime is expected', () => { let concept = factory.newConcept('org.acme.sample', 'Concepts'); @@ -673,7 +1058,6 @@ describe('Serializer', () => { }).should.throw('Model violation in org.acme.sample.Dictionary. Expected Type of String but found \'1234\' instead.'); }); - it('should throw if a bad value is Supplied for Map - where Value type Boolean is expected', () => { let concept = factory.newConcept('org.acme.sample', 'Concepts'); @@ -731,203 +1115,7 @@ describe('Serializer', () => { }); }); - describe('#fromJSON', () => { - - it('should deserialize a JSON object with a Map ', () => { - let json = { - $class: 'org.acme.sample.Concepts', - dict: { - '$class': 'org.acme.sample.Dictionary', - 'Lorem': 'Ipsum', - 'Ipsum': 'Lorem' - } - }; - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.dict.should.be.an.instanceOf(Map); - resource.dict.get('$class').should.equal('org.acme.sample.Dictionary'); - resource.dict.get('Lorem').should.equal('Ipsum'); - }); - - it('should deserialize a JSON object with a Map , where Scalar extends String', () => { - let json = { - $class: 'org.acme.sample.Concepts', - database: { - '$class': 'org.acme.sample.Database', - 'E17B69D9-9B57-4C4A-957E-8B202D7B6C5A': 'Ipsum', - 'D4F45017-AD2B-416B-AD9F-3B74F7DEA291': 'Lorem' - } - }; - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.database.should.be.an.instanceOf(Map); - resource.database.get('$class').should.equal('org.acme.sample.Database'); - resource.database.get('E17B69D9-9B57-4C4A-957E-8B202D7B6C5A').should.equal('Ipsum'); - resource.database.get('D4F45017-AD2B-416B-AD9F-3B74F7DEA291').should.equal('Lorem'); - }); - - it('should deserialize a JSON object with a Map , where Scalar extends DateTime', () => { - let json = { - $class: 'org.acme.sample.Concepts', - appointment: { - '$class': 'org.acme.sample.Appointment', - '2023-10-28T01:02:03Z': 'Ipsum', - '2023-11-28T01:02:03Z': 'Lorem' - } - }; - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.appointment.should.be.an.instanceOf(Map); - resource.appointment.get('$class').should.equal('org.acme.sample.Appointment'); - resource.appointment.get('2023-10-28T01:02:03Z').should.equal('Ipsum'); - }); - - it('should deserialize a JSON object with a Map , where both Key & Value are Identified Concepts', () => { - let json = { - $class: 'org.acme.sample.Concepts', - marriages: { - '$class': 'org.acme.sample.MarriageRegister', - '{"$class":"org.acme.sample.Person","name":"Bob"}': '{"$class":"org.acme.sample.Person","name":"Alice"}' - } - }; - - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.marriages.should.be.an.instanceOf(Map); - resource.marriages.get('$class').should.equal('org.acme.sample.MarriageRegister'); - - resource.marriages.forEach((value, key) => { - if (!ModelUtil.isSystemProperty(key)) { - key.should.be.an.instanceOf(Resource); - key.name.should.equal('Bob'); - value.should.be.an.instanceOf(Resource); - value.name.should.equal('Alice'); - } - }); - }); - - it('should deserialize a JSON object with a Map ', () => { - let json = { - $class: 'org.acme.sample.Concepts', - grade: { - '$class': 'org.acme.sample.ExaminationGrade', - '{"$class":"org.acme.sample.Person","name":"Bob"}': 'A+' - } - }; - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.grade.should.be.an.instanceOf(Map); - resource.grade.get('$class').should.equal('org.acme.sample.ExaminationGrade'); - resource.grade.forEach((value, key) => { - if (!ModelUtil.isSystemProperty(key)) { - key.should.be.an.instanceOf(Resource); - key.name.should.equal('Bob'); - value.should.be.a('String'); - value.should.equal('A+'); - } - }); - }); - - it('should deserialize a JSON object with a Map ', () => { - let json = { - $class: 'org.acme.sample.Concepts', - graduated: { - '$class': 'org.acme.sample.Graduated', - '{"$class":"org.acme.sample.Person","name":"Bob"}': '2023-10-28T01:02:03Z' - } - }; - - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.graduated.should.be.an.instanceOf(Map); - resource.graduated.get('$class').should.equal('org.acme.sample.Graduated'); - resource.graduated.forEach((value, key) => { - if (!ModelUtil.isSystemProperty(key)) { - key.should.be.an.instanceOf(Resource); - key.name.should.equal('Bob'); - value.should.be.a('String'); - value.should.equal('2023-10-28T01:02:03Z'); - } - }); - }); - - it('should deserialize a JSON object with a Map ', () => { - let json = { - $class: 'org.acme.sample.Concepts', - reservation: { - '$class': 'org.acme.sample.Reservation', - '{"$class":"org.acme.sample.Person","name":"Bob"}': '2023-10-28T01:02:03Z' - } - }; - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.reservation.should.be.an.instanceOf(Map); - resource.reservation.get('$class').should.equal('org.acme.sample.Reservation'); - resource.reservation.forEach((value, key) => { - if (!ModelUtil.isSystemProperty(key)) { - key.should.be.an.instanceOf(Resource); - key.name.should.equal('Bob'); - value.should.be.a('String'); - value.should.equal('2023-10-28T01:02:03Z'); - } - }); - }); - - it('should deserialize a JSON object with a Map ', () => { - let json = { - $class: 'org.acme.sample.Concepts', - vip: { - '$class': 'org.acme.sample.GuestList', - '{"$class":"org.acme.sample.Person","name":"Bob"}': true - } - }; - - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.vip.should.be.an.instanceOf(Map); - resource.vip.get('$class').should.equal('org.acme.sample.GuestList'); - resource.vip.forEach((value, key) => { - if (!ModelUtil.isSystemProperty(key)) { - key.should.be.an.instanceOf(Resource); - key.name.should.equal('Bob'); - value.should.be.a('Boolean'); - value.should.equal(true); - } - }); - }); - - it('should deserialize a JSON object with a Map , where Value is a Non-Identified Concept', () => { - let json = { - $class: 'org.acme.sample.Concepts', - team: { - '$class': 'org.acme.sample.Team', - '{"$class":"org.acme.sample.Person","name":"Bob"}': '{"$class":"org.acme.sample.Person","name":"Alice"}' - } - }; - - let resource = serializer.fromJSON(json); - - resource.should.be.an.instanceOf(Resource); - resource.team.should.be.an.instanceOf(Map); - resource.team.get('$class').should.equal('org.acme.sample.Team'); - resource.team.forEach((value, key) => { - if (!ModelUtil.isSystemProperty(key)) { - key.should.be.an.instanceOf(Resource); - key.name.should.equal('Bob'); - value.should.be.an.instanceOf(Resource); - value.name.should.equal('Alice'); - } - }); - }); - + describe('#fromJSON failure scenarios', () => { it('should throw an error when deserializing a Map without a $class property', () => { let json = { $class: 'org.acme.sample.Concepts', @@ -967,7 +1155,7 @@ describe('Serializer', () => { }; (() => { serializer.fromJSON(json); - }).should.throw('TODO ADD CORRECT ERROR MESSAGE HERE'); + }).should.throw('Unexpected properties for type org.acme.sample.Concepts: stateLog'); }); }); }); From 427658f77805dd6b8f15bd3c2d5ec40c7b402f86 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 10 Aug 2023 16:31:52 +0100 Subject: [PATCH 25/41] feat(map): cleanup Signed-off-by: jonathan.casey --- packages/concerto-core/test/serializer/maptype/serializer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/concerto-core/test/serializer/maptype/serializer.js b/packages/concerto-core/test/serializer/maptype/serializer.js index ad4866d14..fec672dba 100644 --- a/packages/concerto-core/test/serializer/maptype/serializer.js +++ b/packages/concerto-core/test/serializer/maptype/serializer.js @@ -19,7 +19,6 @@ const ModelManager = require('../../../lib/modelmanager'); const Resource = require('../../../lib/model/resource'); const Serializer = require('../../../lib/serializer'); const Util = require('../../composer/composermodelutility'); -// const ModelUtil = require('../../../../concerto-core/lib/modelutil'); require('chai').should(); const sinon = require('sinon'); From 539e3936dbcc3455ff5bed701931ce8bfa0f52e6 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 10 Aug 2023 23:29:01 +0100 Subject: [PATCH 26/41] feat(map): DRY up code in the JSONPopulator Signed-off-by: jonathan.casey --- .../lib/serializer/jsonpopulator.js | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index 2c4987465..7a90b0977 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -187,32 +187,12 @@ class JSONPopulator { return; } - if (!ModelUtil.isSystemProperty(key) && !ModelUtil.isPrimitiveType(mapDeclaration.getKey().getType())) { - let decl = mapDeclaration.getModelFile() - .getAllDeclarations() - .find(decl => decl.name === mapDeclaration.getKey().getType()); - if (decl?.isClassDeclaration()) { - let subResource = parameters.factory.newConcept(decl.getNamespace(), - decl.getName(), decl.getIdentifierFieldName() ); - parameters.jsonStack.push(JSON.parse(key)); - parameters.resourceStack.push(subResource); - key = decl.accept(this, parameters); - } + if (!ModelUtil.isPrimitiveType(mapDeclaration.getKey().getType())) { + key = this.processMapType(mapDeclaration, parameters, key, mapDeclaration.getKey().getType()); } if (!ModelUtil.isPrimitiveType(mapDeclaration.getValue().getType())) { - let decl = mapDeclaration.getModelFile() - .getAllDeclarations() - .find(decl => decl.name === mapDeclaration.getValue().getType()); - - if (decl?.isClassDeclaration()) { - let subResource = parameters.factory.newConcept(decl.getNamespace(), - decl.getName(), decl.getIdentifierFieldName() ); - - parameters.jsonStack.push(JSON.parse(value)); - parameters.resourceStack.push(subResource); - value = decl.accept(this, parameters); - } + value = this.processMapType(mapDeclaration, parameters, value, mapDeclaration.getValue().getType()); } map.set(key, value); @@ -221,6 +201,32 @@ class JSONPopulator { return map; } + /** + * Visitor design pattern + * @param {MapDeclaration} mapDeclaration - the object being visited + * @param {Object} parameters - the parameter + * @param {Object} value - the key or value belonging to the Map Entry. + * @param {Object} type - the Type associated with the Key or Value Map Entry. + * @return {Object} value - the key or value belonging to the Map Entry. + */ + processMapType(mapDeclaration, parameters, value, type) { + let decl = mapDeclaration.getModelFile() + .getAllDeclarations() + .find(decl => decl.name === type); + + // if its a ClassDeclaration, populate the Concept. + if (decl?.isClassDeclaration()) { + let subResource = parameters.factory.newConcept(decl.getNamespace(), + decl.getName(), decl.getIdentifierFieldName() ); + + parameters.jsonStack.push(JSON.parse(value)); + parameters.resourceStack.push(subResource); + return decl.accept(this, parameters); + } + // otherwise its a scalar value, we only need to return the primitve value of the scalar. + return value; + } + /** * Visitor design pattern * @param {Field} field - the object being visited From bdeeb022973bff915f4b90d809f374dabb21f9d7 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 10 Aug 2023 23:29:27 +0100 Subject: [PATCH 27/41] feat(map): adds coverage to new introspection functions Signed-off-by: jonathan.casey --- .../test/introspect/mapdeclaration.js | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/concerto-core/test/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index 966ca2186..3bdab6108 100644 --- a/packages/concerto-core/test/introspect/mapdeclaration.js +++ b/packages/concerto-core/test/introspect/mapdeclaration.js @@ -27,6 +27,7 @@ const ModelManager = require('../../lib/modelmanager'); const Util = require('../composer/composermodelutility'); const sinon = require('sinon'); +const expect = require('chai').expect; describe('MapDeclaration', () => { @@ -425,7 +426,7 @@ describe('MapDeclaration', () => { clz.getKey().ast.$class.should.equal('concerto.metamodel@1.0.0.StringMapKeyType'); }); - it('should return the correct values when called - String', () => { + it('should return the correct Type when called - String', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -439,7 +440,7 @@ describe('MapDeclaration', () => { clz.getKey().getType().should.equal('String'); }); - it('should return the correct values when called - DateTime', () => { + it('should return the correct Type when called - DateTime', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -454,15 +455,30 @@ describe('MapDeclaration', () => { }); - it('should return the correct values when called - Scalar DateTime', () => { + it('should return the correct Type when called - Scalar DateTime', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.scalar.datetime.cto', MapDeclaration); decl.getKey().getType().should.equal('DATE'); }); - it('should return the correct values when called - Scalar String', () => { + it('should return the correct Type when called - Scalar String', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodkey.scalar.string.cto', MapDeclaration); decl.getKey().getType().should.equal('GUID'); }); + + it('should return the correct boolean value introspecting isValue or isKey', () => { + let clz = new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.DoubleMapValueType' + } + }); + expect(clz.getKey().isKey()).to.be.true; + expect(clz.getKey().isValue()).to.be.false; + }); }); describe('#getValue', () => { @@ -481,7 +497,7 @@ describe('MapDeclaration', () => { clz.getValue().ast.$class.should.equal('concerto.metamodel@1.0.0.StringMapValueType'); }); - it('should return the correct values when called (Boolean)', () => { + it('should return the correct Type when called - Boolean', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -495,7 +511,7 @@ describe('MapDeclaration', () => { clz.getValue().getType().should.equal('Boolean'); }); - it('should return the correct values when called (DateTime)', () => { + it('should return the correct Type when called - DateTime', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -509,7 +525,7 @@ describe('MapDeclaration', () => { clz.getValue().getType().should.equal('DateTime'); }); - it('should return the correct values when called (String)', () => { + it('should return the correct Type when called - String', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -523,7 +539,7 @@ describe('MapDeclaration', () => { clz.getValue().getType().should.equal('String'); }); - it('should return the correct values when called (Integer)', () => { + it('should return the correct Type when called - Integer', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -537,7 +553,7 @@ describe('MapDeclaration', () => { clz.getValue().getType().should.equal('Integer'); }); - it('should return the correct values when called (Long)', () => { + it('should return the correct Type when called - Long', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -551,7 +567,7 @@ describe('MapDeclaration', () => { clz.getValue().getType().should.equal('Long'); }); - it('should return the correct values when called (Double)', () => { + it('should return the correct Type when called - Double', () => { let clz = new MapDeclaration(modelFile, { $class: 'concerto.metamodel@1.0.0.MapDeclaration', name: 'MapPermutation1', @@ -574,6 +590,21 @@ describe('MapDeclaration', () => { let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.goodvalue.scalar.string.cto', MapDeclaration); decl.getValue().getType().should.equal('GUID'); }); + + it('should return the correct boolean value introspecting isValue or isKey', () => { + let clz = new MapDeclaration(modelFile, { + $class: 'concerto.metamodel@1.0.0.MapDeclaration', + name: 'MapPermutation1', + key: { + $class: 'concerto.metamodel@1.0.0.StringMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.DoubleMapValueType' + } + }); + expect(clz.getValue().isValue()).to.be.true; + expect(clz.getValue().isKey()).to.be.false; + }); }); describe('#Introspect', () => { From b01e444022e6bf446fa174ea144c6b7d196d1839 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 10 Aug 2023 23:35:15 +0100 Subject: [PATCH 28/41] feat(map): adds type definitions Signed-off-by: jonathan.casey --- .../test/serializer/maptype/serializer.js | 4 ---- .../types/lib/introspect/mapdeclaration.d.ts | 20 +++++++++++++------ .../types/lib/introspect/mapkeytype.d.ts | 3 ++- .../types/lib/introspect/mapvaluetype.d.ts | 3 ++- .../types/lib/serializer/jsonpopulator.d.ts | 9 +++++++++ 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/concerto-core/test/serializer/maptype/serializer.js b/packages/concerto-core/test/serializer/maptype/serializer.js index fec672dba..0c3d8456e 100644 --- a/packages/concerto-core/test/serializer/maptype/serializer.js +++ b/packages/concerto-core/test/serializer/maptype/serializer.js @@ -32,13 +32,10 @@ describe('Serializer', () => { beforeEach(() => { process.env.ENABLE_MAP_TYPE = 'true'; - - sandbox = sinon.createSandbox(); modelManager = new ModelManager(); Util.addComposerModel(modelManager); - modelManager.addCTOModel(` namespace org.acme.sample @@ -1143,7 +1140,6 @@ describe('Serializer', () => { }); it('should throw for Enums as Map key types', () => { - let json = { $class: 'org.acme.sample.Concepts', stateLog: { diff --git a/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts b/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts index 905695c23..dad97512c 100644 --- a/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts @@ -68,12 +68,6 @@ declare class MapDeclaration { * @return {string} the Map Value property */ getValue(): string; - /** - * Returns the MapDeclaration properties - * - * @return {array} the MapDeclaration properties - */ - getProperties(): any[]; /** * Returns the string representation of this class * @return {String} the string representation of the class @@ -91,6 +85,20 @@ declare class MapDeclaration { * @return {boolean} true if the class is a class */ isMapDeclaration(): boolean; + /** + * Returns true if this Key is a valid Map Key. + * + * @param {Object} key - the Key of the Map Declaration + * @return {boolean} true if the Key is a valid Map Key + */ + isValidMapKey(key: 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 + */ + isValidMapValue(value: any): boolean; } import ModelFile = require("./modelfile"); import MapKeyType = require("./mapkeytype"); diff --git a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts index 3f6c47f19..2b6e890a7 100644 --- a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts @@ -17,7 +17,6 @@ declare class MapKeyType extends Decorated { */ constructor(parent: MapDeclaration, ast: any); parent: MapDeclaration; - type: any; /** * Semantic validation of the structure of this class. * @@ -25,6 +24,7 @@ declare class MapKeyType extends Decorated { * @protected */ protected validate(): void; + type: string; /** * Returns the owner of this property * @public @@ -50,6 +50,7 @@ declare class MapKeyType extends Decorated { * @return {boolean} true if the class is a Map Value */ isValue(): boolean; + #private; } import Decorated = require("./decorated"); import MapDeclaration = require("./mapdeclaration"); diff --git a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts index 750f109f3..345754da0 100644 --- a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts @@ -16,7 +16,6 @@ declare class MapValueType extends Decorated { */ constructor(parent: MapDeclaration, ast: any); parent: MapDeclaration; - type: any; /** * Semantic validation of the structure of this class. * @@ -24,6 +23,7 @@ declare class MapValueType extends Decorated { * @protected */ protected validate(): void; + type: string; /** * Returns the owner of this property * @public @@ -49,6 +49,7 @@ declare class MapValueType extends Decorated { * @return {boolean} true if the class is a Map Value */ isValue(): boolean; + #private; } import Decorated = require("./decorated"); import MapDeclaration = require("./mapdeclaration"); diff --git a/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts b/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts index fbb38528b..e5cd3d224 100644 --- a/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts +++ b/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts @@ -49,6 +49,15 @@ declare class JSONPopulator { * @private */ private visitMapDeclaration; + /** + * Visitor design pattern + * @param {MapDeclaration} mapDeclaration - the object being visited + * @param {Object} parameters - the parameter + * @param {Object} value - the key or value belonging to the Map Entry. + * @param {Object} type - the Type associated with the Key or Value Map Entry. + * @return {Object} value - the key or value belonging to the Map Entry. + */ + processMapType(mapDeclaration: MapDeclaration, parameters: any, value: any, type: any): any; /** * Visitor design pattern * @param {Field} field - the object being visited From 9bce0140b58250c80398b05bfdde9f60c4603814 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 10 Aug 2023 23:35:53 +0100 Subject: [PATCH 29/41] feat(map): updates changelog Signed-off-by: jonathan.casey --- packages/concerto-core/api.txt | 3 ++- packages/concerto-core/changelog.txt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index decb297ac..0aa4c8193 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -162,10 +162,11 @@ class MapDeclaration extends Declaration { + string getName() + string getKey() + string getValue() - + array getProperties() + String toString() + string declarationKind() + boolean isMapDeclaration() + + boolean isValidMapKey(Object) + + boolean isValidMapValue(Object) } class MapKeyType extends Decorated { + void constructor(MapDeclaration,Object,ModelFile) throws IllegalModelException diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index 14994e61f..34c381208 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.8.2 {1a497711c4cca84d1dfa92c0b581fadf} 2023-08-10 +- Add isValidMapKey(Object) isValidMapValue(Object) + Version 3.8.1 {794268f69b81f05f711d38a9ef1a7833} 2023-08-1 - Add To MapKeyType, MapValue functionality From 56204f68bb8c688df0db59e0458b1ef0a6b8041e Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 10 Aug 2023 23:46:38 +0100 Subject: [PATCH 30/41] fix: Private identifiers are only available when targeting ECMAScript 2015 and higher Signed-off-by: jonathan.casey --- packages/concerto-core/lib/introspect/mapkeytype.js | 5 ++--- packages/concerto-core/lib/introspect/mapvaluetype.js | 5 ++--- .../concerto-core/types/lib/introspect/mapkeytype.d.ts | 7 ++++++- .../concerto-core/types/lib/introspect/mapvaluetype.d.ts | 7 ++++++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index 4dbb49047..86bc75e87 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -58,7 +58,7 @@ class MapKeyType extends Decorated { */ process() { super.process(); - this.#processType(this.ast); + this.processType(this.ast); } /** @@ -97,10 +97,9 @@ class MapKeyType extends Decorated { /** * Sets the Type name for the Map Key * - * @private * @param {Object} ast - The AST created by the parser */ - #processType(ast) { + processType(ast) { let decl; switch(this.ast.$class) { case `${MetaModelNamespace}.DateTimeMapKeyType`: diff --git a/packages/concerto-core/lib/introspect/mapvaluetype.js b/packages/concerto-core/lib/introspect/mapvaluetype.js index 951a2b46d..64462e7e5 100644 --- a/packages/concerto-core/lib/introspect/mapvaluetype.js +++ b/packages/concerto-core/lib/introspect/mapvaluetype.js @@ -57,7 +57,7 @@ class MapValueType extends Decorated { */ process() { super.process(); - this.#processType(this.ast); + this.processType(this.ast); } /** @@ -82,10 +82,9 @@ class MapValueType extends Decorated { /** * Sets the Type name for the Map Value * - * @private * @param {Object} ast - The AST created by the parser */ - #processType(ast) { + processType(ast) { let decl; switch(this.ast.$class) { case `${MetaModelNamespace}.ObjectMapValueType`: diff --git a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts index 2b6e890a7..371f2f42f 100644 --- a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts @@ -24,6 +24,12 @@ declare class MapKeyType extends Decorated { * @protected */ protected validate(): void; + /** + * Sets the Type name for the Map Key + * + * @param {Object} ast - The AST created by the parser + */ + processType(ast: any): void; type: string; /** * Returns the owner of this property @@ -50,7 +56,6 @@ declare class MapKeyType extends Decorated { * @return {boolean} true if the class is a Map Value */ isValue(): boolean; - #private; } import Decorated = require("./decorated"); import MapDeclaration = require("./mapdeclaration"); diff --git a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts index 345754da0..d45a3b637 100644 --- a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts @@ -23,6 +23,12 @@ declare class MapValueType extends Decorated { * @protected */ protected validate(): void; + /** + * Sets the Type name for the Map Value + * + * @param {Object} ast - The AST created by the parser + */ + processType(ast: any): void; type: string; /** * Returns the owner of this property @@ -49,7 +55,6 @@ declare class MapValueType extends Decorated { * @return {boolean} true if the class is a Map Value */ isValue(): boolean; - #private; } import Decorated = require("./decorated"); import MapDeclaration = require("./mapdeclaration"); From 0d495d5597dd0a765de796e635fa7f29cadeca46 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Thu, 10 Aug 2023 23:55:17 +0100 Subject: [PATCH 31/41] fix: updates changelog Signed-off-by: jonathan.casey --- packages/concerto-core/api.txt | 2 ++ packages/concerto-core/changelog.txt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 0aa4c8193..2157551d1 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -171,6 +171,7 @@ class MapDeclaration extends Declaration { class MapKeyType extends Decorated { + void constructor(MapDeclaration,Object,ModelFile) throws IllegalModelException ~ void validate() throws IllegalModelException + + void processType(Object) + ModelFile getModelFile() + MapDeclaration getParent() + string getType() @@ -181,6 +182,7 @@ class MapKeyType extends Decorated { class MapValueType extends Decorated { + void constructor(MapDeclaration,Object) throws IllegalModelException ~ void validate() throws IllegalModelException + + void processType(Object) + ModelFile getModelFile() + MapDeclaration getParent() + string getType() diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index 34c381208..bfde0fa89 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.8.3 {7dd828f29c49cedfb8eee2f3c6fefa6e} 2023-08-10 +- Removes private identifiers from MapKeyType & MapValueType + Version 3.8.2 {1a497711c4cca84d1dfa92c0b581fadf} 2023-08-10 - Add isValidMapKey(Object) isValidMapValue(Object) From f72a3d032cd8e19b59365a08e6d19e827798f22f Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Mon, 14 Aug 2023 12:16:30 +0100 Subject: [PATCH 32/41] feat(map): moves isValidMapKey & isValidMapValue to ModelUtils Signed-off-by: jonathan.casey --- packages/concerto-core/api.txt | 2 - packages/concerto-core/changelog.txt | 4 ++ .../lib/introspect/mapdeclaration.js | 38 +------------------ packages/concerto-core/lib/modelutil.js | 33 ++++++++++++++++ .../types/lib/introspect/mapdeclaration.d.ts | 14 ------- .../concerto-core/types/lib/modelutil.d.ts | 14 +++++++ 6 files changed, 53 insertions(+), 52 deletions(-) diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 2157551d1..6e0016a15 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -165,8 +165,6 @@ class MapDeclaration extends Declaration { + String toString() + string declarationKind() + boolean isMapDeclaration() - + boolean isValidMapKey(Object) - + boolean isValidMapValue(Object) } class MapKeyType extends Decorated { + void constructor(MapDeclaration,Object,ModelFile) throws IllegalModelException diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index bfde0fa89..d227ad507 100644 --- a/packages/concerto-core/changelog.txt +++ b/packages/concerto-core/changelog.txt @@ -24,6 +24,10 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # + +Version 3.8.4 {a99786670d2561bf2b84cb3eb1bb1a08} 2023-08-14 +- Move isValidMapKey & isValidMapValue to ModelUtils + Version 3.8.3 {7dd828f29c49cedfb8eee2f3c6fefa6e} 2023-08-10 - Removes private identifiers from MapKeyType & MapValueType diff --git a/packages/concerto-core/lib/introspect/mapdeclaration.js b/packages/concerto-core/lib/introspect/mapdeclaration.js index bbc339457..a83e735aa 100644 --- a/packages/concerto-core/lib/introspect/mapdeclaration.js +++ b/packages/concerto-core/lib/introspect/mapdeclaration.js @@ -14,8 +14,6 @@ 'use strict'; -const { MetaModelNamespace } = require('@accordproject/concerto-metamodel'); - const Declaration = require('./declaration'); const IllegalModelException = require('./illegalmodelexception'); const MapValueType = require('./mapvaluetype'); @@ -69,11 +67,11 @@ class MapDeclaration extends Declaration { throw new IllegalModelException(`MapDeclaration must contain Key & Value properties ${this.ast.name}`, this.modelFile, this.ast.location); } - if (!this.isValidMapKey(this.ast.key)) { + if (!ModelUtil.isValidMapKey(this.ast.key)) { throw new IllegalModelException(`MapDeclaration must contain valid MapKeyType ${this.ast.name}`, this.modelFile, this.ast.location); } - if (!this.isValidMapValue(this.ast.value)) { + if (!ModelUtil.isValidMapValue(this.ast.value)) { throw new IllegalModelException(`MapDeclaration must contain valid MapValueType, for MapDeclaration ${this.ast.name}` , this.modelFile, this.ast.location); } @@ -168,38 +166,6 @@ class MapDeclaration extends Declaration { isMapDeclaration() { return true; } - - /** - * Returns true if this Key is a valid Map Key. - * - * @param {Object} key - the Key of the Map Declaration - * @return {boolean} true if the Key is a valid Map Key - */ - isValidMapKey(key) { - return [ - `${MetaModelNamespace}.StringMapKeyType`, - `${MetaModelNamespace}.DateTimeMapKeyType`, - `${MetaModelNamespace}.ObjectMapKeyType`, - ].includes(key.$class); - } - - /** - * 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 - */ - isValidMapValue(value) { - return [ - `${MetaModelNamespace}.BooleanMapValueType`, - `${MetaModelNamespace}.DateTimeMapValueType`, - `${MetaModelNamespace}.StringMapValueType`, - `${MetaModelNamespace}.IntegerMapValueType`, - `${MetaModelNamespace}.LongMapValueType`, - `${MetaModelNamespace}.DoubleMapValueType`, - `${MetaModelNamespace}.ObjectMapValueType` - ].includes(value.$class); - } } module.exports = MapDeclaration; diff --git a/packages/concerto-core/lib/modelutil.js b/packages/concerto-core/lib/modelutil.js index 8defb7b99..6971d4b81 100644 --- a/packages/concerto-core/lib/modelutil.js +++ b/packages/concerto-core/lib/modelutil.js @@ -14,6 +14,7 @@ 'use strict'; +const { MetaModelNamespace } = require('@accordproject/concerto-metamodel'); const { MetaModelUtil } = require('@accordproject/concerto-metamodel'); const semver = require('semver'); const Globalize = require('./globalize'); @@ -286,6 +287,38 @@ class ModelUtil { static isPrivateSystemProperty(propertyName) { return privateReservedProperties.includes(propertyName); } + + /** + * Returns true if this Key is a valid Map Key. + * + * @param {Object} key - the Key of the Map Declaration + * @return {boolean} true if the Key is a valid Map Key + */ + static isValidMapKey(key) { + return [ + `${MetaModelNamespace}.StringMapKeyType`, + `${MetaModelNamespace}.DateTimeMapKeyType`, + `${MetaModelNamespace}.ObjectMapKeyType`, + ].includes(key.$class); + } + + /** + * 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`, + `${MetaModelNamespace}.DateTimeMapValueType`, + `${MetaModelNamespace}.StringMapValueType`, + `${MetaModelNamespace}.IntegerMapValueType`, + `${MetaModelNamespace}.LongMapValueType`, + `${MetaModelNamespace}.DoubleMapValueType`, + `${MetaModelNamespace}.ObjectMapValueType` + ].includes(value.$class); + } } module.exports = ModelUtil; diff --git a/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts b/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts index dad97512c..6932af382 100644 --- a/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts @@ -85,20 +85,6 @@ declare class MapDeclaration { * @return {boolean} true if the class is a class */ isMapDeclaration(): boolean; - /** - * Returns true if this Key is a valid Map Key. - * - * @param {Object} key - the Key of the Map Declaration - * @return {boolean} true if the Key is a valid Map Key - */ - isValidMapKey(key: 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 - */ - isValidMapValue(value: any): boolean; } import ModelFile = require("./modelfile"); import MapKeyType = require("./mapkeytype"); diff --git a/packages/concerto-core/types/lib/modelutil.d.ts b/packages/concerto-core/types/lib/modelutil.d.ts index b3ab5b3f5..402e26de1 100644 --- a/packages/concerto-core/types/lib/modelutil.d.ts +++ b/packages/concerto-core/types/lib/modelutil.d.ts @@ -141,4 +141,18 @@ declare class ModelUtil { * @private */ private static isPrivateSystemProperty; + /** + * Returns true if this Key is a valid Map Key. + * + * @param {Object} key - the Key of the Map Declaration + * @return {boolean} true if the Key is a valid Map Key + */ + static isValidMapKey(key: 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 + */ + static isValidMapValue(value: any): boolean; } From 32951fc93199699412ea9a1e2c63ccedf7249531 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Mon, 14 Aug 2023 13:37:52 +0100 Subject: [PATCH 33/41] feat(map): removes unreachable default case Signed-off-by: jonathan.casey --- packages/concerto-core/lib/introspect/mapkeytype.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index 86bc75e87..69c2cc3f6 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -112,10 +112,6 @@ class MapKeyType extends Decorated { decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type.name); this.type = decl.getName(); break; - default: - decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type.name); - this.type = decl.getName(); - break; } } From a4d3749636225f514aab9f4cb06b0cc8539f7139 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Mon, 14 Aug 2023 13:59:12 +0100 Subject: [PATCH 34/41] feat(map): removes unreachable code Signed-off-by: jonathan.casey --- packages/concerto-core/lib/introspect/mapkeytype.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index 69c2cc3f6..7870bbed5 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -80,12 +80,6 @@ class MapKeyType extends Decorated { ); } - if(decl.isMapDeclaration?.()) { - throw new IllegalModelException( - `MapDeclaration as MapKeyType is not supported, for MapDeclaration ${this.parent.name}` - ); - } - if (decl?.isConcept?.() || decl?.isClassDeclaration?.()) { throw new IllegalModelException( `MapKeyType supports String and DateTime only,for MapDeclaration ${this.parent.name}` From af42e117d056dd3114a8214bb542cbe01c6ea3bf Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Mon, 14 Aug 2023 18:22:14 +0100 Subject: [PATCH 35/41] feat(map): adds more test coverage Signed-off-by: jonathan.casey --- .../test/introspect/mapdeclaration.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/concerto-core/test/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index 3bdab6108..77d437e90 100644 --- a/packages/concerto-core/test/introspect/mapdeclaration.js +++ b/packages/concerto-core/test/introspect/mapdeclaration.js @@ -230,6 +230,41 @@ describe('MapDeclaration', () => { }).should.throw(IllegalModelException); }); + it('should throw if ast contains illegal Map Key Property - 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', () => { + (() => { + 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', () => { + (() => { + 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', () => { + (() => { + 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', () => { + (() => { + 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', () => { (() => { new MapDeclaration(modelFile, { From 926f91de6d01298565202fa409a6fefde70a1d96 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Mon, 14 Aug 2023 18:43:39 +0100 Subject: [PATCH 36/41] feat(map): remove dead code Signed-off-by: jonathan.casey --- .../concerto-core/lib/serializer/jsongenerator.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/concerto-core/lib/serializer/jsongenerator.js b/packages/concerto-core/lib/serializer/jsongenerator.js index 65584b581..36e3c2345 100644 --- a/packages/concerto-core/lib/serializer/jsongenerator.js +++ b/packages/concerto-core/lib/serializer/jsongenerator.js @@ -91,23 +91,13 @@ class JSONGenerator { obj.forEach((value, key) => { + // don't serialize System Properties, other than $class if(ModelUtil.isSystemProperty(key) && key !== '$class') { return; } - if (typeof key === 'object') { - let decl = mapDeclaration.getModelFile() - .getAllDeclarations() - .find(decl => decl.name === key.getType()); - - // convert declaration to JSON representation - parameters.stack.push(key); - const jsonKey = decl.accept(this, parameters); - - key = JSON.stringify(jsonKey); - } - + // Key is always a string, but value might be a ValidatedResource. if (typeof value === 'object') { let decl = mapDeclaration.getModelFile() .getAllDeclarations() From 04b079888a0bb0afa79d186a1005d91fbd798da0 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Mon, 14 Aug 2023 22:28:06 +0100 Subject: [PATCH 37/41] feat(map): adds JSDoc @private Signed-off-by: jonathan.casey --- packages/concerto-core/lib/introspect/mapkeytype.js | 1 + packages/concerto-core/lib/introspect/mapvaluetype.js | 1 + packages/concerto-core/lib/serializer/jsonpopulator.js | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index 7870bbed5..9c7169e1e 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -92,6 +92,7 @@ class MapKeyType extends Decorated { * Sets the Type name for the Map Key * * @param {Object} ast - The AST created by the parser + * @private */ processType(ast) { let decl; diff --git a/packages/concerto-core/lib/introspect/mapvaluetype.js b/packages/concerto-core/lib/introspect/mapvaluetype.js index 64462e7e5..8b2898a05 100644 --- a/packages/concerto-core/lib/introspect/mapvaluetype.js +++ b/packages/concerto-core/lib/introspect/mapvaluetype.js @@ -83,6 +83,7 @@ class MapValueType extends Decorated { * Sets the Type name for the Map Value * * @param {Object} ast - The AST created by the parser + * @private */ processType(ast) { let decl; diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index 7a90b0977..734b2494a 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -208,6 +208,7 @@ class JSONPopulator { * @param {Object} value - the key or value belonging to the Map Entry. * @param {Object} type - the Type associated with the Key or Value Map Entry. * @return {Object} value - the key or value belonging to the Map Entry. + * @private */ processMapType(mapDeclaration, parameters, value, type) { let decl = mapDeclaration.getModelFile() From d446e1d4fd990bab8334778d07c3cf397462170f Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Mon, 14 Aug 2023 22:42:49 +0100 Subject: [PATCH 38/41] feat(map): adds type defs & change log Signed-off-by: jonathan.casey --- packages/concerto-core/api.txt | 2 -- packages/concerto-core/changelog.txt | 2 ++ packages/concerto-core/types/lib/introspect/mapkeytype.d.ts | 3 ++- packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts | 3 ++- packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts | 3 ++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 6e0016a15..10062d3ba 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -169,7 +169,6 @@ class MapDeclaration extends Declaration { class MapKeyType extends Decorated { + void constructor(MapDeclaration,Object,ModelFile) throws IllegalModelException ~ void validate() throws IllegalModelException - + void processType(Object) + ModelFile getModelFile() + MapDeclaration getParent() + string getType() @@ -180,7 +179,6 @@ class MapKeyType extends Decorated { class MapValueType extends Decorated { + void constructor(MapDeclaration,Object) throws IllegalModelException ~ void validate() throws IllegalModelException - + void processType(Object) + ModelFile getModelFile() + MapDeclaration getParent() + string getType() diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index d227ad507..4e229504d 100644 --- a/packages/concerto-core/changelog.txt +++ b/packages/concerto-core/changelog.txt @@ -24,6 +24,8 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 3.8.5 {9cd54e67c4a39a3041706e4b37dce9f1} 2023-08-14 +- Makes MapKeyType MapValueType processType() private Version 3.8.4 {a99786670d2561bf2b84cb3eb1bb1a08} 2023-08-14 - Move isValidMapKey & isValidMapValue to ModelUtils diff --git a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts index 371f2f42f..e1461945f 100644 --- a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts @@ -28,8 +28,9 @@ declare class MapKeyType extends Decorated { * Sets the Type name for the Map Key * * @param {Object} ast - The AST created by the parser + * @private */ - processType(ast: any): void; + private processType; type: string; /** * Returns the owner of this property diff --git a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts index d45a3b637..d199a3b2d 100644 --- a/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts +++ b/packages/concerto-core/types/lib/introspect/mapvaluetype.d.ts @@ -27,8 +27,9 @@ declare class MapValueType extends Decorated { * Sets the Type name for the Map Value * * @param {Object} ast - The AST created by the parser + * @private */ - processType(ast: any): void; + private processType; type: string; /** * Returns the owner of this property diff --git a/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts b/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts index e5cd3d224..ce45eba64 100644 --- a/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts +++ b/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts @@ -56,8 +56,9 @@ declare class JSONPopulator { * @param {Object} value - the key or value belonging to the Map Entry. * @param {Object} type - the Type associated with the Key or Value Map Entry. * @return {Object} value - the key or value belonging to the Map Entry. + * @private */ - processMapType(mapDeclaration: MapDeclaration, parameters: any, value: any, type: any): any; + private processMapType; /** * Visitor design pattern * @param {Field} field - the object being visited From 19dfcf76dd0d4e9a6b4ac657dece4cee987e948d Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Wed, 16 Aug 2023 11:42:28 +0100 Subject: [PATCH 39/41] feat(map): improve error message Signed-off-by: jonathan.casey --- packages/concerto-core/lib/introspect/mapkeytype.js | 2 +- packages/concerto-core/lib/serializer/jsongenerator.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index 9c7169e1e..09d8568f6 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -82,7 +82,7 @@ class MapKeyType extends Decorated { if (decl?.isConcept?.() || decl?.isClassDeclaration?.()) { throw new IllegalModelException( - `MapKeyType supports String and DateTime only,for MapDeclaration ${this.parent.name}` + `Invalid Map key type in MapDeclaration ${this.parent.name}. Only String and DateTime types are supported for Map key types` ); } } diff --git a/packages/concerto-core/lib/serializer/jsongenerator.js b/packages/concerto-core/lib/serializer/jsongenerator.js index 36e3c2345..85fad1f8a 100644 --- a/packages/concerto-core/lib/serializer/jsongenerator.js +++ b/packages/concerto-core/lib/serializer/jsongenerator.js @@ -91,7 +91,6 @@ class JSONGenerator { obj.forEach((value, key) => { - // don't serialize System Properties, other than $class if(ModelUtil.isSystemProperty(key) && key !== '$class') { return; From 74cb9978d11427db0a6db0309ee93a5dd62397bb Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Wed, 16 Aug 2023 12:13:55 +0100 Subject: [PATCH 40/41] feat(map): add util method Signed-off-by: jonathan.casey --- packages/concerto-core/lib/introspect/mapkeytype.js | 4 +--- packages/concerto-core/lib/modelutil.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index 09d8568f6..81f647711 100644 --- a/packages/concerto-core/lib/introspect/mapkeytype.js +++ b/packages/concerto-core/lib/introspect/mapkeytype.js @@ -72,9 +72,7 @@ class MapKeyType extends Decorated { if (!ModelUtil.isPrimitiveType(this.type)) { let decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type?.name); - if (decl?.isScalarDeclaration?.() && - !(decl?.ast.$class === `${MetaModelNamespace}.StringScalar`) && - !(decl?.ast.$class === `${MetaModelNamespace}.DateTimeScalar`)) { + if (!ModelUtil.isValidMapKeyScalar(decl)) { throw new IllegalModelException( `Scalar must be one of StringScalar, DateTimeScalar in context of MapKeyType. Invalid Scalar: ${this.type}, for MapDeclaration ${this.parent.name}` ); diff --git a/packages/concerto-core/lib/modelutil.js b/packages/concerto-core/lib/modelutil.js index 6971d4b81..502badb58 100644 --- a/packages/concerto-core/lib/modelutil.js +++ b/packages/concerto-core/lib/modelutil.js @@ -302,6 +302,18 @@ class ModelUtil { ].includes(key.$class); } + /** + * Returns true if this Key is a valid Map Key Scalar Value. + * + * @param {Object} decl - the Map Key Scalar declaration + * @return {boolean} true if the Key is a valid Map Key Scalar type + */ + static isValidMapKeyScalar(decl) { + return (decl?.isScalarDeclaration?.() && + (decl?.ast.$class !== `${MetaModelNamespace}.StringScalar` || + decl?.ast.$class !== `${MetaModelNamespace}.DateTimeScalar`)); + } + /** * Returns true if this Value is a valid Map Value. * From 08ce775f133dbcaae7b0030e75759f06ab246830 Mon Sep 17 00:00:00 2001 From: "jonathan.casey" Date: Wed, 16 Aug 2023 12:17:05 +0100 Subject: [PATCH 41/41] fix: update type def Signed-off-by: jonathan.casey --- packages/concerto-core/types/lib/modelutil.d.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/concerto-core/types/lib/modelutil.d.ts b/packages/concerto-core/types/lib/modelutil.d.ts index 402e26de1..473d2c255 100644 --- a/packages/concerto-core/types/lib/modelutil.d.ts +++ b/packages/concerto-core/types/lib/modelutil.d.ts @@ -148,6 +148,13 @@ declare class ModelUtil { * @return {boolean} true if the Key is a valid Map Key */ static isValidMapKey(key: any): boolean; + /** + * Returns true if this Key is a valid Map Key Scalar Value. + * + * @param {Object} decl - the Map Key Scalar declaration + * @return {boolean} true if the Key is a valid Map Key Scalar type + */ + static isValidMapKeyScalar(decl: any): boolean; /** * Returns true if this Value is a valid Map Value. *