diff --git a/packages/concerto-core/lib/modelutil.js b/packages/concerto-core/lib/modelutil.js index 109436e881..8defb7b992 100644 --- a/packages/concerto-core/lib/modelutil.js +++ b/packages/concerto-core/lib/modelutil.js @@ -191,7 +191,7 @@ class ModelUtil { } /** - * Returns the true if the given field is an enumerated type + * Returns true if the given field is an enumerated type * @param {Field} field - the string * @return {boolean} true if the field is declared as an enumeration * @private @@ -199,11 +199,23 @@ class ModelUtil { static isEnum(field) { const modelFile = field.getParent().getModelFile(); const typeDeclaration = modelFile.getType(field.getType()); - return (typeDeclaration !== null && typeDeclaration.isEnum()); + return typeDeclaration?.isEnum(); } /** - * Returns the true if the given field is a Scalar type + * Returns true if the given field is an map type + * @param {Field} field - the string + * @return {boolean} true if the field is declared as an map + * @private + */ + static isMap(field) { + const modelFile = field.getParent().getModelFile(); + const typeDeclaration = modelFile.getType(field.getType()); + return typeDeclaration?.isMapDeclaration?.(); + } + + /** + * Returns true if the given field is a Scalar type * @param {Field} field - the Field to test * @return {boolean} true if the field is declared as an scalar * @private @@ -211,7 +223,7 @@ class ModelUtil { static isScalar(field) { const modelFile = field.getParent().getModelFile(); const declaration = modelFile.getType(field.getType()); - return (declaration !== null && declaration.isScalarDeclaration?.()); + return declaration?.isScalarDeclaration?.(); } /** diff --git a/packages/concerto-core/lib/serializer.js b/packages/concerto-core/lib/serializer.js index a289ac51ff..817ba5d6ec 100644 --- a/packages/concerto-core/lib/serializer.js +++ b/packages/concerto-core/lib/serializer.js @@ -161,18 +161,20 @@ class Serializer { // create a new instance, using the identifier field name as the ID. let resource; - if (classDeclaration.isTransaction()) { + if (classDeclaration.isTransaction?.()) { resource = this.factory.newTransaction(classDeclaration.getNamespace(), classDeclaration.getName(), jsonObject[classDeclaration.getIdentifierFieldName()] ); - } else if (classDeclaration.isEvent()) { + } else if (classDeclaration.isEvent?.()) { resource = this.factory.newEvent(classDeclaration.getNamespace(), classDeclaration.getName(), jsonObject[classDeclaration.getIdentifierFieldName()] ); - } else if (classDeclaration.isConcept()) { + } else if (classDeclaration.isConcept?.()) { resource = this.factory.newConcept(classDeclaration.getNamespace(), classDeclaration.getName(), jsonObject[classDeclaration.getIdentifierFieldName()] ); + } else if (classDeclaration.isMapDeclaration?.()) { + throw new Error('Attempting to create a Map declaration is not supported.'); } else if (classDeclaration.isEnum()) { throw new Error('Attempting to create an ENUM declaration is not supported.'); } else { diff --git a/packages/concerto-core/lib/serializer/instancegenerator.js b/packages/concerto-core/lib/serializer/instancegenerator.js index 35acf11292..1055eaf8ac 100644 --- a/packages/concerto-core/lib/serializer/instancegenerator.js +++ b/packages/concerto-core/lib/serializer/instancegenerator.js @@ -37,6 +37,8 @@ class InstanceGenerator { visit(thing, parameters) { if (thing.isClassDeclaration?.()) { return this.visitClassDeclaration(thing, parameters); + } else if (thing.isMapDeclaration?.()) { + return this.visitMapDeclaration(thing, parameters); } else if (thing.isRelationship?.()) { return this.visitRelationshipDeclaration(thing, parameters); } else if (thing.isTypeScalar?.()) { @@ -152,31 +154,33 @@ class InstanceGenerator { } } - let classDeclaration = parameters.modelManager.getType(type); + let declaration = parameters.modelManager.getType(type); - if (classDeclaration.isEnum()) { - let enumValues = classDeclaration.getOwnProperties(); + if (declaration.isEnum()) { + let enumValues = declaration.getOwnProperties(); return parameters.valueGenerator.getEnum(enumValues).getName(); } - classDeclaration = this.findConcreteSubclass(classDeclaration); + declaration = this.findConcreteSubclass(declaration); - let id = null; - if (classDeclaration.isIdentified()) { - let idFieldName = classDeclaration.getIdentifierFieldName(); - let idField = classDeclaration.getProperty(idFieldName); - if (idField?.isTypeScalar?.()){ - idField = idField.getScalarField(); - } - if(idField?.validator?.regex){ - id = parameters.valueGenerator.getRegex(fieldOrScalarDeclaration.validator.regex); - } else { - id = this.generateRandomId(classDeclaration); + if (!declaration.isMapDeclaration?.()) { + let id = null; + if (declaration.isIdentified()) { + let idFieldName = declaration.getIdentifierFieldName(); + let idField = declaration.getProperty(idFieldName); + if (idField?.isTypeScalar?.()){ + idField = idField.getScalarField(); + } + if(idField?.validator?.regex){ + id = parameters.valueGenerator.getRegex(fieldOrScalarDeclaration.validator.regex); + } else { + id = this.generateRandomId(declaration); + } } + let resource = parameters.factory.newResource(declaration.getNamespace(), declaration.getName(), id); + parameters.stack.push(resource); } - let resource = parameters.factory.newResource(classDeclaration.getNamespace(), classDeclaration.getName(), id); - parameters.stack.push(resource); - return classDeclaration.accept(this, parameters); + return declaration.accept(this, parameters); } /** @@ -190,7 +194,7 @@ class InstanceGenerator { * @throws {Error} if no concrete subclasses exist. */ findConcreteSubclass(declaration) { - if (!declaration.isAbstract()) { + if (declaration.isMapDeclaration?.() || !declaration.isAbstract()) { return declaration; } @@ -229,6 +233,17 @@ class InstanceGenerator { } } + /** + * 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) { + return parameters.valueGenerator.getMap(); + } + /** * Generate a random ID for a given type. * @private diff --git a/packages/concerto-core/lib/serializer/jsongenerator.js b/packages/concerto-core/lib/serializer/jsongenerator.js index f8fd6c5408..33db7c1e4a 100644 --- a/packages/concerto-core/lib/serializer/jsongenerator.js +++ b/packages/concerto-core/lib/serializer/jsongenerator.js @@ -65,6 +65,8 @@ class JSONGenerator { return this.visitClassDeclaration(thing, parameters); } else if (thing.isRelationship?.()) { return this.visitRelationshipDeclaration(thing, parameters); + }else if (thing.isMapDeclaration?.()) { + return this.visitMapDeclaration(thing, parameters); } else if (thing.isTypeScalar?.()) { return this.visitField(thing.getScalarField(), parameters); } else if (thing.isField?.()) { @@ -74,6 +76,18 @@ class JSONGenerator { } } + /** + * 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(); + return { $class: obj.$class, value: Object.fromEntries(obj.value)}; + } + /** * Visitor design pattern * @param {ClassDeclaration} classDeclaration - the object being visited @@ -148,7 +162,12 @@ class JSONGenerator { result = this.convertToJSON(field, obj); } else if (ModelUtil.isEnum(field)) { result = this.convertToJSON(field, obj); - } else { + } else if (ModelUtil.isMap(field)) { + parameters.stack.push(obj); + const mapDeclaration = parameters.modelManager.getType(field.getFullyQualifiedTypeName()); + result = mapDeclaration.accept(this, parameters); + } + else { parameters.stack.push(obj); const classDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType()); result = classDeclaration.accept(this, parameters); diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index b82d66a1df..eab4515e5f 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -121,6 +121,8 @@ class JSONPopulator { if (thing.isClassDeclaration?.()) { return this.visitClassDeclaration(thing, parameters); + } else if (thing.isMapDeclaration?.()) { + return this.visitMapDeclaration(thing, parameters); } else if (thing.isRelationship?.()) { return this.visitRelationshipDeclaration(thing, parameters); } else if (thing.isTypeScalar?.()) { @@ -160,6 +162,29 @@ class JSONPopulator { return resourceObj; } + /** + * 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 jsonObj = parameters.jsonStack.pop(); + parameters.path ?? (parameters.path = new TypedStack('$')); + const path = parameters.path.stack.join(''); + + if(!jsonObj.$class) { + throw new Error(`Invalid JSON data at "${path}". Map value does not contain a $class type identifier.`); + } + + if(!jsonObj.value) { + throw new Error(`Invalid JSON data at "${path}". Map value does not contain a value property.`); + } + + return { $class: jsonObj.$class, value: new Map(Object.entries(jsonObj.value)) }; + } + /** * Visitor design pattern * @param {Field} field - the object being visited @@ -197,7 +222,7 @@ class JSONPopulator { convertItem(field, jsonItem, parameters) { let result = null; - if(!field.isPrimitive() && !field.isTypeEnum()) { + if(!field.isPrimitive?.() && !field.isTypeEnum?.()) { let typeName = jsonItem.$class; if(!typeName) { // If the type name is not specified in the data, then use the @@ -207,26 +232,26 @@ class JSONPopulator { } // This throws if the type does not exist. - const classDeclaration = parameters.modelManager.getType(typeName); + const declaration = parameters.modelManager.getType(typeName); - // create a new instance, using the identifier field name as the ID. - let subResource = null; + if (!declaration.isMapDeclaration?.()) { - // if this is identifiable, then we create a resource - if(classDeclaration.isIdentified()) { - subResource = parameters.factory.newResource(classDeclaration.getNamespace(), - classDeclaration.getName(), jsonItem[classDeclaration.getIdentifierFieldName()] ); - } - else { - // otherwise we create a concept - subResource = parameters.factory.newConcept(classDeclaration.getNamespace(), - classDeclaration.getName() ); - } + // create a new instance, using the identifier field name as the ID. + let subResource = null; - result = subResource; - parameters.resourceStack.push(subResource); + // if this is identifiable, then we create a resource + if (declaration.isIdentified()) { + subResource = parameters.factory.newResource(declaration.getNamespace(), + declaration.getName(), jsonItem[declaration.getIdentifierFieldName()] ); + } else { + // otherwise we create a concept + subResource = parameters.factory.newConcept(declaration.getNamespace(), + declaration.getName()); + } + parameters.resourceStack.push(subResource); + } parameters.jsonStack.push(jsonItem); - classDeclaration.accept(this, parameters); + result = declaration.accept(this, parameters); } else { result = this.convertToObject(field, jsonItem, parameters); diff --git a/packages/concerto-core/lib/serializer/resourcevalidator.js b/packages/concerto-core/lib/serializer/resourcevalidator.js index eb9083d131..baaedac125 100644 --- a/packages/concerto-core/lib/serializer/resourcevalidator.js +++ b/packages/concerto-core/lib/serializer/resourcevalidator.js @@ -67,7 +67,9 @@ class ResourceValidator { return this.visitEnumDeclaration(thing, parameters); } else if (thing.isClassDeclaration?.()) { return this.visitClassDeclaration(thing, parameters); - } else if (thing.isRelationship?.()) { + } else if (thing.isMapDeclaration?.()) { + return this.visitMapDeclaration(thing, parameters); + }else if (thing.isRelationship?.()) { return this.visitRelationshipDeclaration(thing, parameters); } else if (thing.isTypeScalar?.()) { return this.visitField(thing.getScalarField(), parameters); @@ -104,6 +106,36 @@ class ResourceValidator { return null; } + /** + * Visitor design pattern + * + * @param {MapDeclaration} mapDeclaration - the object being visited + * @param {Object} parameters - the parameter + * @private + */ + visitMapDeclaration(mapDeclaration, parameters) { + const obj = parameters.stack.pop(); + + if (!((obj.value instanceof Map))) { + throw new Error('Expected a Map, but found ' + JSON.stringify(obj)); + } + + if (obj.$class !== mapDeclaration.getFullyQualifiedName()) { + throw new Error(`$class value must match ${mapDeclaration.getFullyQualifiedName()}`); + } + + obj.value.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); + } + } + }); + } + /** * Visitor design pattern * @param {ClassDeclaration} classDeclaration - the object being visited @@ -450,7 +482,7 @@ class ResourceValidator { /** * Throw a new error for a model violation. * @param {string} id - the identifier of this instance. - * @param {classDeclaration} classDeclaration - the declaration of the classs + * @param {ClassDeclaration} classDeclaration - the declaration of the class * @param {Object} value - the value of the field. * @private */ @@ -466,7 +498,23 @@ class ResourceValidator { /** * Throw a new error for a model violation. * @param {string} id - the identifier of this instance. - * @param {RelationshipDeclaration} relationshipDeclaration - the declaration of the classs + * @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. + * @param {RelationshipDeclaration} relationshipDeclaration - the declaration of the class * @param {Object} value - the value of the field. * @private */ diff --git a/packages/concerto-core/lib/serializer/valuegenerator.js b/packages/concerto-core/lib/serializer/valuegenerator.js index 9993170c4a..ef2e3bb76c 100644 --- a/packages/concerto-core/lib/serializer/valuegenerator.js +++ b/packages/concerto-core/lib/serializer/valuegenerator.js @@ -230,6 +230,14 @@ class EmptyValueGenerator { return enumValues[0]; } + /** + * Get an instance of an empty map. + * @return {*} an map value. + */ + getMap() { + return new Map(); + } + /** * Get an array using the supplied callback to obtain array values. * @param {Function} valueSupplier - callback to obtain values. @@ -328,6 +336,14 @@ class SampleValueGenerator extends EmptyValueGenerator { return enumValues[Math.floor(Math.random() * enumValues.length)]; } + /** + * Get a map instance with randomly generated values for key & value. + * @return {*} a map value. + */ + getMap() { + return new Map([[this.getString(1,10), this.getString(1,10)]]); + } + /** * Get an array using the supplied callback to obtain array values. * @param {Function} valueSupplier - callback to obtain values. diff --git a/packages/concerto-core/messages/en.json b/packages/concerto-core/messages/en.json index 01325dbc86..910b20822e 100644 --- a/packages/concerto-core/messages/en.json +++ b/packages/concerto-core/messages/en.json @@ -69,6 +69,7 @@ "resourcevalidator-undeclaredfield": "Instance \"{resourceId}\" has a property named \"{propertyName}\", which is not declared in \"{fullyQualifiedTypeName}\".", "resourcevalidator-invalidfieldassignment": "Instance \"{resourceId}\" has a property \"{propertyName}\" with type \"{objectType}\" that is not derived from \"{fieldType}\".", "resourcevalidator-emptyidentifier": "Instance \"{resourceId}\" has an empty identifier.", + "resourcevalidator-invalidmap": "Model violation in the \"{resourceId}\" instance. Invalid Type for Map Key or Value - expected String type.", "typenotfounderror-defaultmessage": "Type \"{typeName}\" not found.", diff --git a/packages/concerto-core/test/data/model/concept.cto b/packages/concerto-core/test/data/model/concept.cto index 357dc3b78c..0bed15fdd1 100644 --- a/packages/concerto-core/test/data/model/concept.cto +++ b/packages/concerto-core/test/data/model/concept.cto @@ -22,6 +22,7 @@ concept InventorySets { o String Model o Integer invCount o assetStatus invType // used or new? + o Dictionary dictionary optional } asset MakerInventory identified by makerId { @@ -35,3 +36,8 @@ enum assetStatus { o REPAIRED o RETIRED } + +map Dictionary { + o String + o String +} diff --git a/packages/concerto-core/test/model/concept.js b/packages/concerto-core/test/model/concept.js index 347a82309c..d426a35b49 100644 --- a/packages/concerto-core/test/model/concept.js +++ b/packages/concerto-core/test/model/concept.js @@ -128,6 +128,40 @@ describe('Concept', function () { }).should.throw(/Attempting to create an ENUM declaration is not supported./); }); + it('should generate a concept with a Map from JSON', function () { + let conceptModel = fs.readFileSync('./test/data/model/concept.cto', 'utf8'); + modelManager.addCTOModel(conceptModel, 'concept.cto'); + const factory = new Factory(modelManager); + const serializer = new Serializer(factory, modelManager); + const jsObject = { + $class:'org.acme.biznet.InventorySets', + Make:'Make', + Model:'Model', + invCount:10, + invType:'NEWBATCH', + dictionary: { + $class: 'org.acme.biznet.Dictionary', + value: { + 'key1': 'value1', + 'key2': 'value2', + } + } + }; + const obj = serializer.fromJSON(jsObject); + obj.isConcept().should.be.true; + }); + + it('should generate an error trying to create a Map from JSON', function () { + let conceptModel = fs.readFileSync('./test/data/model/concept.cto', 'utf8'); + modelManager.addCTOModel(conceptModel, 'concept.cto'); + const factory = new Factory(modelManager); + const serializer = new Serializer(factory, modelManager); + const jsObject = JSON.parse('{"$class":"org.acme.biznet.Dictionary"}'); + (function () { + serializer.fromJSON(jsObject); + }).should.throw(/Attempting to create a Map declaration is not supported./); + }); + }); describe('#isConcept', () => { diff --git a/packages/concerto-core/test/serializer.js b/packages/concerto-core/test/serializer.js index 939a0058e0..cb41f274a8 100644 --- a/packages/concerto-core/test/serializer.js +++ b/packages/concerto-core/test/serializer.js @@ -66,6 +66,17 @@ 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 { @@ -232,6 +243,60 @@ 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 = { + $class: 'org.acme.sample.Dictionary', + value: new Map(Object.entries({'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', + value: { '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 = { + $class: 'org.acme.sample.PhoneBook', + value: new Map(Object.entries({'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'); @@ -332,6 +397,72 @@ 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', + value: { + '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.$class.should.equal('org.acme.sample.Dictionary'); + resource.dict.value.should.be.an.instanceOf(Map); + resource.dict.value.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', + value: { + 'Lorem': 'Ipsum' + } + } + }; + (() => { + serializer.fromJSON(json); + }).should.throw('Invalid JSON data at "$.dict". Map value does not contain a $class type identifier.'); + }); + + it('should throw an error when deserializing a map without a value property', () => { + let json = { + $class: 'org.acme.sample.Address', + city: 'Winchester', + country: 'UK', + elevation: 3.14, + postcode: 'SO21 2JN', + dict: { + '$class': 'org.acme.sample.Dictionary', + // value: { + // 'Lorem': 'Ipsum' + // } + } + }; + (() => { + serializer.fromJSON(json); + }).should.throw('Invalid JSON data at "$.dict". Map value does not contain a value property.'); + }); + it('should throw validation errors if the validate flag is not specified', () => { let json = { $class: 'org.acme.sample.SampleAsset', diff --git a/packages/concerto-core/test/serializer/instancegenerator.js b/packages/concerto-core/test/serializer/instancegenerator.js index 8adb90e007..87a7b4582c 100644 --- a/packages/concerto-core/test/serializer/instancegenerator.js +++ b/packages/concerto-core/test/serializer/instancegenerator.js @@ -88,6 +88,29 @@ describe('InstanceGenerator', () => { resource.theValue.should.be.a('string'); }); + it('should generate default value for a Map', function () { + let resource = test(`namespace org.acme.test + + map Foo { + o String + o String + } + + concept MyAsset identified by assetId { + o String assetId + o Foo bar + } + `); + resource.bar.should.be.an.instanceOf(Map); + resource.bar.size.should.be.equal(1); + + const iterator1 = resource.bar.entries(); + let values = iterator1.next().value; + + values[0].should.be.a('string'); + values[1].should.be.a('string'); + }); + it('should generate a value with specified lentgh constraint for a string property', () => { useSampleGenerator(); let resource = test(`namespace org.acme.test diff --git a/packages/concerto-core/test/serializer/resourcevalidator.js b/packages/concerto-core/test/serializer/resourcevalidator.js index 027b52cf5e..8398a95305 100644 --- a/packages/concerto-core/test/serializer/resourcevalidator.js +++ b/packages/concerto-core/test/serializer/resourcevalidator.js @@ -50,6 +50,15 @@ describe('ResourceValidator', function () { o DEER_OTHER }`; + const mapModelString = `namespace org.acme.map + map PhoneBook { + o String + o String + } + concept Data { + o PhoneBook numbers + }`; + const levelOneModel = `namespace org.acme.l1 enum VehicleType { o CAR @@ -114,6 +123,7 @@ describe('ResourceValidator', function () { }); beforeEach(function () { + modelManager.addCTOModel(mapModelString); modelManager.addCTOModel(enumModelString); modelManager.addCTOModel(levelOneModel); modelManager.addCTOModel(levelTwoModel); @@ -336,6 +346,39 @@ describe('ResourceValidator', function () { }); }); + describe('#visitMapDeclaration', function() { + it('should validate map', function () { + const map = { $class: 'org.acme.map.PhoneBook', value: new Map([['Lorem', 'Ipsum']]) }; + const typedStack = new TypedStack(map); + const mapDeclaration = modelManager.getType('org.acme.map.PhoneBook'); + const parameters = { stack : typedStack, 'modelManager' : modelManager, rootResourceIdentifier : 'TEST' }; + mapDeclaration.accept(resourceValidator,parameters ); + }); + + it('should not validate map with bad value', function () { + const map = { $class: 'org.acme.map.PhoneBook', value: new Map([['Lorem', 3]]) }; + const typedStack = new TypedStack(map); + const mapDeclaration = modelManager.getType('org.acme.map.PhoneBook'); + const parameters = { stack : typedStack, 'modelManager' : modelManager, rootResourceIdentifier : 'TEST' }; + + (() => { + mapDeclaration.accept(resourceValidator,parameters ); + }).should.throw('Model violation in the "TEST" instance. Invalid Type for Map Key or Value - expected String type.'); + }); + + it('should not validate map with bad key', function () { + const map = { $class: 'org.acme.map.PhoneBook', value: new Map([[1, 'Ipsum']]) }; + + const typedStack = new TypedStack(map); + const mapDeclaration = modelManager.getType('org.acme.map.PhoneBook'); + const parameters = { stack : typedStack, 'modelManager' : modelManager, rootResourceIdentifier : 'TEST' }; + + (() => { + mapDeclaration.accept(resourceValidator,parameters ); + }).should.throw('Model violation in the "TEST" instance. Invalid Type for Map Key or Value - expected String type.'); + }); + }); + describe('#visitClassDeclaration', function() { it('should detect visiting a non resource', function () { diff --git a/packages/concerto-core/test/serializer/valuegenerator.js b/packages/concerto-core/test/serializer/valuegenerator.js index 4616aa8ee0..bea94cf433 100644 --- a/packages/concerto-core/test/serializer/valuegenerator.js +++ b/packages/concerto-core/test/serializer/valuegenerator.js @@ -62,6 +62,10 @@ describe('ValueGenerator', function() { assertFunctionReturnsType('getBoolean', 'boolean'); }); + it('getMap should return a map', function() { + assertFunctionReturnsType('getMap', 'Map'); + }); + it('getString should return a string', function() { assertFunctionReturnsType('getString', 'string'); }); diff --git a/packages/concerto-core/types/lib/modelutil.d.ts b/packages/concerto-core/types/lib/modelutil.d.ts index 479871e4ed..ae085afbdb 100644 --- a/packages/concerto-core/types/lib/modelutil.d.ts +++ b/packages/concerto-core/types/lib/modelutil.d.ts @@ -91,6 +91,13 @@ declare class ModelUtil { * @private */ private static isEnum; + /** + * Returns the true if the given field is an map type + * @param {Field} field - the string + * @return {boolean} true if the field is declared as an map + * @private + */ + private static isMap; /** * Returns the true if the given field is a Scalar type * @param {Field} field - the Field to test diff --git a/packages/concerto-core/types/lib/serializer/instancegenerator.d.ts b/packages/concerto-core/types/lib/serializer/instancegenerator.d.ts index f612270761..f4f9cb8b7f 100644 --- a/packages/concerto-core/types/lib/serializer/instancegenerator.d.ts +++ b/packages/concerto-core/types/lib/serializer/instancegenerator.d.ts @@ -58,6 +58,14 @@ declare class InstanceGenerator { * @private */ private visitRelationshipDeclaration; + /** + * 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; /** * Generate a random ID for a given type. * @private diff --git a/packages/concerto-core/types/lib/serializer/jsongenerator.d.ts b/packages/concerto-core/types/lib/serializer/jsongenerator.d.ts index 60313f3c94..4a3c86e29d 100644 --- a/packages/concerto-core/types/lib/serializer/jsongenerator.d.ts +++ b/packages/concerto-core/types/lib/serializer/jsongenerator.d.ts @@ -38,6 +38,14 @@ declare class JSONGenerator { * @private */ private visit; + /** + * 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; /** * Visitor design pattern * @param {ClassDeclaration} classDeclaration - the object being visited diff --git a/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts b/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts index df634fca25..fbb38528b2 100644 --- a/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts +++ b/packages/concerto-core/types/lib/serializer/jsonpopulator.d.ts @@ -41,6 +41,14 @@ declare class JSONPopulator { * @private */ private visitClassDeclaration; + /** + * 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; /** * 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 7183d5e44a..b5c2f58546 100644 --- a/packages/concerto-core/types/lib/serializer/resourcevalidator.d.ts +++ b/packages/concerto-core/types/lib/serializer/resourcevalidator.d.ts @@ -31,7 +31,7 @@ declare class ResourceValidator { /** * Throw a new error for a model violation. * @param {string} id - the identifier of this instance. - * @param {classDeclaration} classDeclaration - the declaration of the classs + * @param {ClassDeclaration} classDeclaration - the declaration of the class * @param {Object} value - the value of the field. * @private */ @@ -39,7 +39,15 @@ declare class ResourceValidator { /** * Throw a new error for a model violation. * @param {string} id - the identifier of this instance. - * @param {RelationshipDeclaration} relationshipDeclaration - the declaration of the classs + * @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. + * @param {RelationshipDeclaration} relationshipDeclaration - the declaration of the class * @param {Object} value - the value of the field. * @private */ @@ -125,6 +133,14 @@ declare class ResourceValidator { * @private */ private visitEnumDeclaration; + /** + * Visitor design pattern + * + * @param {MapDeclaration} mapDeclaration - the object being visited + * @param {Object} parameters - the parameter + * @private + */ + private visitMapDeclaration; /** * Visitor design pattern * @param {ClassDeclaration} classDeclaration - the object being visited