From 1ba9c07b3f9ca796b8bb0622d9023a48f34bd4e4 Mon Sep 17 00:00:00 2001 From: Jonathan-Casey <109082377+Jonathan-Casey@users.noreply.github.com> Date: Tue, 25 Jul 2023 14:41:38 +0100 Subject: [PATCH] feat(map): refactor map shape (#674) * feat(map): refactor map shape Signed-off-by: jonathan.casey * feat(*): map can be empty Signed-off-by: jonathan.casey * feat(*): map cannot use private reserved properties as keys Signed-off-by: jonathan.casey * feat(*): refactor test Signed-off-by: jonathan.casey --------- Signed-off-by: jonathan.casey --- .../lib/serializer/jsongenerator.js | 2 +- .../lib/serializer/jsonpopulator.js | 14 ++--- .../lib/serializer/resourcevalidator.js | 11 ++-- packages/concerto-core/test/model/concept.js | 6 +-- packages/concerto-core/test/serializer.js | 54 +++++++++---------- .../test/serializer/resourcevalidator.js | 7 ++- 6 files changed, 42 insertions(+), 52 deletions(-) diff --git a/packages/concerto-core/lib/serializer/jsongenerator.js b/packages/concerto-core/lib/serializer/jsongenerator.js index 33db7c1e4..2cef9574a 100644 --- a/packages/concerto-core/lib/serializer/jsongenerator.js +++ b/packages/concerto-core/lib/serializer/jsongenerator.js @@ -85,7 +85,7 @@ class JSONGenerator { */ visitMapDeclaration(mapDeclaration, parameters) { const obj = parameters.stack.pop(); - return { $class: obj.$class, value: Object.fromEntries(obj.value)}; + return Object.fromEntries(obj); } /** diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index eab4515e5..33f89fb39 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -46,7 +46,7 @@ function getAssignableProperties(resourceData, classDeclaration) { } if (properties.includes('$timestamp') && - !(classDeclaration.isTransaction() || classDeclaration.isEvent()) + !(classDeclaration.isTransaction?.() || classDeclaration.isEvent?.()) ) { const errorText = `Unexpected property for type ${classDeclaration.getFullyQualifiedName()}: $timestamp`; throw new ValidationException(errorText); @@ -172,17 +172,11 @@ class JSONPopulator { 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.`); - } + // throws if Map contains private reserved properties as keys. + getAssignableProperties(jsonObj, mapDeclaration); - return { $class: jsonObj.$class, value: new Map(Object.entries(jsonObj.value)) }; + return new Map(Object.entries(jsonObj)); } /** diff --git a/packages/concerto-core/lib/serializer/resourcevalidator.js b/packages/concerto-core/lib/serializer/resourcevalidator.js index baaedac12..d303d3294 100644 --- a/packages/concerto-core/lib/serializer/resourcevalidator.js +++ b/packages/concerto-core/lib/serializer/resourcevalidator.js @@ -116,15 +116,20 @@ class ResourceValidator { visitMapDeclaration(mapDeclaration, parameters) { const obj = parameters.stack.pop(); - if (!((obj.value instanceof Map))) { + if (!((obj instanceof Map))) { throw new Error('Expected a Map, but found ' + JSON.stringify(obj)); } - if (obj.$class !== mapDeclaration.getFullyQualifiedName()) { + if(!obj.has('$class')) { + throw new Error('Invalid Map. Map must contain a properly formatted $class property'); + } + + if (obj.get('$class') !== mapDeclaration.getFullyQualifiedName()) { throw new Error(`$class value must match ${mapDeclaration.getFullyQualifiedName()}`); } - obj.value.forEach((value, key) => { + + obj.forEach((value, key) => { if(!ModelUtil.isSystemProperty(key)) { if (typeof key !== 'string') { ResourceValidator.reportInvalidMap(parameters.rootResourceIdentifier, mapDeclaration, obj); diff --git a/packages/concerto-core/test/model/concept.js b/packages/concerto-core/test/model/concept.js index d426a35b4..e04810487 100644 --- a/packages/concerto-core/test/model/concept.js +++ b/packages/concerto-core/test/model/concept.js @@ -141,10 +141,8 @@ describe('Concept', function () { invType:'NEWBATCH', dictionary: { $class: 'org.acme.biznet.Dictionary', - value: { - 'key1': 'value1', - 'key2': 'value2', - } + key1: 'value1', + key2: 'value2', } }; const obj = serializer.fromJSON(jsObject); diff --git a/packages/concerto-core/test/serializer.js b/packages/concerto-core/test/serializer.js index cb41f274a..226163d10 100644 --- a/packages/concerto-core/test/serializer.js +++ b/packages/concerto-core/test/serializer.js @@ -243,16 +243,15 @@ describe('Serializer', () => { }); }); - it('should generate concept with a map value', () => { + 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'})) - }; + 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); @@ -264,13 +263,12 @@ describe('Serializer', () => { postcode: 'SO21 2JN', dict: { $class: 'org.acme.sample.Dictionary', - value: { 'Lorem': 'Ipsum' } + Lorem: 'Ipsum' } }); }); - - it('should throw if the value for a map is not a Map instance', () => { + 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'; @@ -288,10 +286,10 @@ describe('Serializer', () => { 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'})) - }; + 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'); @@ -397,8 +395,7 @@ describe('Serializer', () => { resource.postcode.should.equal('SO21 2JN'); }); - it('should deserialize a valid concept with a map', () => { - + it('should deserialize a valid concept with a Map', () => { let json = { $class: 'org.acme.sample.Address', city: 'Winchester', @@ -407,9 +404,7 @@ describe('Serializer', () => { postcode: 'SO21 2JN', dict: { '$class': 'org.acme.sample.Dictionary', - value: { - 'Lorem': 'Ipsum' - } + 'Lorem': 'Ipsum' } }; let resource = serializer.fromJSON(json); @@ -419,12 +414,12 @@ describe('Serializer', () => { 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'); + 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', () => { + it('should throw an error when deserializing a Map without a $class property', () => { let json = { $class: 'org.acme.sample.Address', @@ -434,17 +429,17 @@ describe('Serializer', () => { postcode: 'SO21 2JN', dict: { // '$class': 'org.acme.sample.Dictionary', - value: { - 'Lorem': 'Ipsum' - } + 'Lorem': 'Ipsum' } }; (() => { serializer.fromJSON(json); - }).should.throw('Invalid JSON data at "$.dict". Map value does not contain a $class type identifier.'); + }).should.throw('Invalid Map. Map must contain a properly formatted $class property'); }); - it('should throw an error when deserializing a map without a value property', () => { + + it('should throw an error when deserializing a Map with a private reserved property', () => { + let json = { $class: 'org.acme.sample.Address', city: 'Winchester', @@ -453,14 +448,13 @@ describe('Serializer', () => { postcode: 'SO21 2JN', dict: { '$class': 'org.acme.sample.Dictionary', - // value: { - // 'Lorem': 'Ipsum' - // } + '$namespace': 'com.reserved.property', + 'Lorem': 'Ipsum' } }; (() => { serializer.fromJSON(json); - }).should.throw('Invalid JSON data at "$.dict". Map value does not contain a value property.'); + }).should.throw('Unexpected reserved properties for type org.acme.sample.Dictionary: $namespace'); }); it('should throw validation errors if the validate flag is not specified', () => { diff --git a/packages/concerto-core/test/serializer/resourcevalidator.js b/packages/concerto-core/test/serializer/resourcevalidator.js index 8398a9530..cb34d35aa 100644 --- a/packages/concerto-core/test/serializer/resourcevalidator.js +++ b/packages/concerto-core/test/serializer/resourcevalidator.js @@ -348,7 +348,7 @@ describe('ResourceValidator', function () { describe('#visitMapDeclaration', function() { it('should validate map', function () { - const map = { $class: 'org.acme.map.PhoneBook', value: new Map([['Lorem', 'Ipsum']]) }; + const map = new Map([['$class', 'org.acme.map.PhoneBook'], ['Lorem', 'Ipsum']]); const typedStack = new TypedStack(map); const mapDeclaration = modelManager.getType('org.acme.map.PhoneBook'); const parameters = { stack : typedStack, 'modelManager' : modelManager, rootResourceIdentifier : 'TEST' }; @@ -356,7 +356,7 @@ describe('ResourceValidator', function () { }); it('should not validate map with bad value', function () { - const map = { $class: 'org.acme.map.PhoneBook', value: new Map([['Lorem', 3]]) }; + const map = new Map([['$class', 'org.acme.map.PhoneBook'], ['Lorem', 3]]); const typedStack = new TypedStack(map); const mapDeclaration = modelManager.getType('org.acme.map.PhoneBook'); const parameters = { stack : typedStack, 'modelManager' : modelManager, rootResourceIdentifier : 'TEST' }; @@ -367,8 +367,7 @@ describe('ResourceValidator', function () { }); it('should not validate map with bad key', function () { - const map = { $class: 'org.acme.map.PhoneBook', value: new Map([[1, 'Ipsum']]) }; - + const map = new Map([['$class', 'org.acme.map.PhoneBook'], [1, 'Ipsum']]); const typedStack = new TypedStack(map); const mapDeclaration = modelManager.getType('org.acme.map.PhoneBook'); const parameters = { stack : typedStack, 'modelManager' : modelManager, rootResourceIdentifier : 'TEST' };