diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index b4f35ef91..10062d3ba 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -162,7 +162,6 @@ class MapDeclaration extends Declaration { + string getName() + string getKey() + string getValue() - + array getProperties() + String toString() + string declarationKind() + boolean isMapDeclaration() @@ -174,6 +173,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 +183,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..4e229504d 100644 --- a/packages/concerto-core/changelog.txt +++ b/packages/concerto-core/changelog.txt @@ -24,6 +24,21 @@ # 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 + +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) + +Version 3.8.1 {794268f69b81f05f711d38a9ef1a7833} 2023-08-1 +- Add To MapKeyType, MapValue functionality + Version 3.7.0 {a97cb6ebd45679354ba4da1940d2bb8d} 2023-05-19 - Add MapDeclaration, MapKeyType, AggregateValueType diff --git a/packages/concerto-core/lib/introspect/mapdeclaration.js b/packages/concerto-core/lib/introspect/mapdeclaration.js index 28823c877..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'); @@ -65,24 +63,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 (!ModelUtil.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 (!ModelUtil.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 +141,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 diff --git a/packages/concerto-core/lib/introspect/mapkeytype.js b/packages/concerto-core/lib/introspect/mapkeytype.js index e8a6743da..81f647711 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,44 @@ 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 (!ModelUtil.isPrimitiveType(this.type)) { + let decl = this.parent.getModelFile().getAllDeclarations().find(d => d.name === this.ast.type?.name); + + 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}` + ); + } - 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}` - ); + if (decl?.isConcept?.() || decl?.isClassDeclaration?.()) { + throw new IllegalModelException( + `Invalid Map key type in MapDeclaration ${this.parent.name}. Only String and DateTime types are supported for Map key types` + ); + } } + } + /** + * Sets the Type name for the Map Key + * + * @param {Object} ast - The AST created by the parser + * @private + */ + 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; + } } /** @@ -129,6 +144,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..8b2898a05 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,65 @@ 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 + * + * @param {Object} ast - The AST created by the parser + * @private + */ + processType(ast) { + let decl; + 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 `${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; } } @@ -121,6 +166,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; diff --git a/packages/concerto-core/lib/modelutil.js b/packages/concerto-core/lib/modelutil.js index 8defb7b99..502badb58 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,50 @@ 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 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. + * + * @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/lib/serializer/jsongenerator.js b/packages/concerto-core/lib/serializer/jsongenerator.js index 2cef9574a..85fad1f8a 100644 --- a/packages/concerto-core/lib/serializer/jsongenerator.js +++ b/packages/concerto-core/lib/serializer/jsongenerator.js @@ -85,7 +85,34 @@ 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) => { + + // don't serialize System Properties, other than $class + if(ModelUtil.isSystemProperty(key) && key !== '$class') { + return; + } + + // Key is always a string, but value might be a ValidatedResource. + 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); } /** diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index 33f89fb39..734b2494a 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -170,13 +170,62 @@ 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 (!ModelUtil.isPrimitiveType(mapDeclaration.getKey().getType())) { + key = this.processMapType(mapDeclaration, parameters, key, mapDeclaration.getKey().getType()); + } + + if (!ModelUtil.isPrimitiveType(mapDeclaration.getValue().getType())) { + value = this.processMapType(mapDeclaration, parameters, value, mapDeclaration.getValue().getType()); + } + + map.set(key, value); + }); + + 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. + * @private + */ + 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; } /** diff --git a/packages/concerto-core/lib/serializer/resourcevalidator.js b/packages/concerto-core/lib/serializer/resourcevalidator.js index d303d3294..3df7c4224 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,79 @@ 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()); + + // 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 +190,16 @@ 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)) { + // Validate Key + this.checkMapType(mapDeclaration.getKey(), key, parameters, mapDeclaration); + // Validate Value + this.checkMapType(mapDeclaration.getValue(), value, parameters, mapDeclaration); } }); + + return null; } /** @@ -500,22 +561,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. - * @private - */ - static reportInvalidMap(id, mapDeclaration, value) { - let formatter = Globalize.messageFormatter('resourcevalidator-invalidmap'); - throw new ValidationException(formatter({ - resourceId: id, - classFQN: mapDeclaration.getFullyQualifiedName(), - invalidValue: value.toString() - })); - } - /** * 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.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.goodkey.declaration.concept.cto b/packages/concerto-core/test/data/parser/mapdeclaration/mapdeclaration.badkey.identified.declaration.concept.cto similarity index 88% 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 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.badkey.identified.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 {} 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/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/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/introspect/mapdeclaration.js b/packages/concerto-core/test/introspect/mapdeclaration.js index e473d0824..77d437e90 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', () => { @@ -45,55 +46,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,240 +75,365 @@ 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', () => { - (() => { + + + 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); }); - }); - 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 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); }); + }); - 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 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); + 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', () => { - it('should throw validating with a non-identified concept declaration as key', function() { + describe('#validate failure scenarios - Map Key', () => { + + it('should throw if ast contains illegal Map Key Property', () => { (() => { + 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 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(/ConceptDeclaration must be identified in context of MapKeyType: NotIdentified/); + decl.validate().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); + 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(); - }).should.throw(/MapKeyType has invalid Type: NotDeclared/); + }); + }); + + 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 when map value is an illegal type', function() { + 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', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badvalue.nontype.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapPropertyType has invalid Type: NONTYPE/); + 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 key is a boolean', function() { + it('should throw if ast contains illegal Map Key Property - Integer', () => { (() => { - 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.IntegerMapKeyType' + }, + value: { + $class: 'concerto.metamodel@1.0.0.StringMapValueType' + } + }); + }).should.throw(IllegalModelException); }); - it('should throw when map key is an event declaration', function() { + it('should throw if ast contains illegal Map Key Property - Long', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.event.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapKeyType has invalid Type: Activity/); + 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 when map key is of type MapDeclaration', function() { + it('should throw if ast contains illegal Map Key Property - Double', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.declaration.map.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapKeyType has invalid Type: IllegalMapKey/); + 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 map key is of primitive type Double', function() { + it('should throw if ast contains illegal Map Key Property - Enum', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.double.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapKeyType has invalid Type: Double/); + 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); }); + }); + + describe('#validate failure scenarios - Map Value', () => { - it('should throw when map key is of primitive type Integer', function() { + it('should throw if ObjectMapValueType does not contain type property ', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.integer.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapKeyType has invalid Type: Integer/); + 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 primitive type Long', function() { + it('should throw if ObjectMapValueType TypeIdentifier does not contain name property', () => { (() => { - let decl = introspectUtils.loadLastDeclaration('test/data/parser/mapdeclaration/mapdeclaration.badkey.primitive.long.cto', MapDeclaration); - decl.validate(); - }).should.throw(/MapKeyType has invalid Type: Long/); + 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 when map key is of scalar type Double', function() { + 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:/); }); }); 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,113 +448,211 @@ 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'); }); - it('should return the correct values when called', () => { + it('should return the correct Type when called - String', () => { 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 Type when called - DateTime', () => { + 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'); }); + + + 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 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', () => { 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 Type 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 Type 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 Type 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 Type 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 Type 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 Type 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'); + }); + + 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'); + }); + + 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', () => { 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); @@ -465,25 +660,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); }); 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 new file mode 100644 index 000000000..0c3d8456e --- /dev/null +++ b/packages/concerto-core/test/serializer/maptype/serializer.js @@ -0,0 +1,1157 @@ +/* + * 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'); + +require('chai').should(); +const sinon = require('sinon'); + +describe('Serializer', () => { + + let sandbox; + let factory; + let modelManager; + 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 + o Timer timer optional + o StopWatch stopwatch optional + o RSVP rsvp optional + o Database database optional + o Appointment appointment optional + o Birthday birthday optional + o Celebration celebration optional + o Rolodex rolodex optional + o Directory directory optional + o Score score optional + o Points points optional + o Balance balance optional + } + + scalar GUID extends String + + scalar Time extends DateTime + + scalar PostalCode extends String + + concept Person identified by name { + o String name + } + + map Dictionary { + 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 Database { + o GUID + o String + } + + 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 Rolodex { + o String + o Person + } + + map Score { + o String + o Integer + } + + map Points { + o String + o Long + } + + map Balance { + o String + o Double + } + + `); + factory = new Factory(modelManager); + + serializer = new Serializer(factory, modelManager); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('# toJSON <> fromJSON', () => { + it('should serialize -> deserialize with a Map ', () => { + // setup + 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'); + concept.dict.set('Ipsum', 'Lorem'); + + // serialize and assert + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + dict: { + $class: 'org.acme.sample.Dictionary', + Lorem: 'Ipsum', + 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 serialize -> deserialize with a Map ', () => { + // setup + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + 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', + 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 serialize -> deserialize with a Map ', () => { + // setup + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + 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', + 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 serialize -> deserialize with a Map ', () => { + // setup + let concept = factory.newConcept('org.acme.sample', 'Concepts'); + + 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', + 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 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('Bob', true); + concept.rsvp.set('Alice', false); + + // serialize and assert + const json = serializer.toJSON(concept); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + 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 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('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', + 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 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('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', + 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 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('Dublin', bob); + concept.rolodex.set('London', alice); + + // serialize & assert + const json = serializer.toJSON(concept); + + 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"}' + } + }); + + // 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 serialize -> deserialize with a Map : Scalar extends DateTime', () => { + // 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-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', + 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 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', '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': '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 serialize -> deserialize with a Map ', () => { + // 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: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', + 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 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.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); + + // serialize and assert + 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"}', + } + }); + + // 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 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', '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', + 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'); + }); + }); + + 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' + } + }; + + // 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('Bob').should.equal('Ipsum'); + resource.dict.get('Alice').should.equal('Lorem'); + + // serialize and assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + dict: { + $class: 'org.acme.sample.Dictionary', + Bob: 'Ipsum', + Alice: 'Lorem' + } + }); + }); + + 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 + } + }; + + // 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); + + // serialize and assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + score: { + $class: 'org.acme.sample.Score', + Bob: 1, + Alice: 1 + } + }); + }); + + 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 + } + }; + + // 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); + + // serialize and assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + points: { + $class: 'org.acme.sample.Points', + Bob: -398741129664271, + Alice: 8999999125356546 + } + }); + }); + + 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 + } + }; + + // 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); + + // serialize and assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + balance: { + $class: 'org.acme.sample.Balance', + Bob: 99999.99, + Alice: 1000000.00 + } + }); + }); + + 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 + } + }; + + // 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); + + // serialize and assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + rsvp: { + $class: 'org.acme.sample.RSVP', + Bob: true, + Alice: false + } + }); + }); + + 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' + } + }; + + // 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' + } + }; + + // 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'); + + // serialize & assert + json = serializer.toJSON(resource); + + json.should.deep.equal({ + $class: 'org.acme.sample.Concepts', + 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'); + + 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'); + }); + + 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 failure scenarios', () => { + 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'); + }); + + 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('Unexpected properties for type org.acme.sample.Concepts: stateLog'); + }); + }); +}); + 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'); }); }); diff --git a/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts b/packages/concerto-core/types/lib/introspect/mapdeclaration.d.ts index 905695c23..6932af382 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 diff --git a/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts b/packages/concerto-core/types/lib/introspect/mapkeytype.d.ts index 600b3605d..e1461945f 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,14 @@ 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 + * @private + */ + private processType; + type: string; /** * Returns the owner of this property * @public @@ -38,6 +45,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..d199a3b2d 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,14 @@ 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 + * @private + */ + private processType; + type: string; /** * Returns the owner of this property * @public @@ -37,6 +44,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/modelutil.d.ts b/packages/concerto-core/types/lib/modelutil.d.ts index b3ab5b3f5..473d2c255 100644 --- a/packages/concerto-core/types/lib/modelutil.d.ts +++ b/packages/concerto-core/types/lib/modelutil.d.ts @@ -141,4 +141,25 @@ 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 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. + * + * @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; } diff --git a/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts b/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts index fbb38528b..ce45eba64 100644 --- a/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts +++ b/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts @@ -49,6 +49,16 @@ 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. + * @private + */ + private processMapType; /** * Visitor design pattern * @param {Field} field - the object being visited 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; 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 __ "}" 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`) { 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 + } + ] + } ] - } +}