diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 4593d9f504..c6d78cdbe9 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -196,6 +196,6 @@ class ModelManager { class Serializer { + void constructor(Factory,ModelManager) + void setDefaultOptions(Object) - + Object toJSON(Resource,Object,boolean,boolean,boolean,boolean) throws Error + + Object toJSON(Resource,Object,boolean,boolean,boolean,boolean,boolean) throws Error + Resource fromJSON(Object,Object,boolean,boolean) } diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index c151b4072b..ad2da17558 100644 --- a/packages/concerto-core/changelog.txt +++ b/packages/concerto-core/changelog.txt @@ -24,6 +24,9 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 0.80.3 {6f5a9ab45943cb76732c14b11f47d044} 2019-08-24 +- Add Ergo option to serializer + Version 0.80.1 {297c88d29ce911ec6efc0f28ceeeb660} 2019-08-24 - Adds getModels and writeModelsToFileSystem functions to ModelManager - Fixes API generation for hasSymbol function diff --git a/packages/concerto-core/lib/serializer.js b/packages/concerto-core/lib/serializer.js index b1d97f5834..fb9eef78f7 100644 --- a/packages/concerto-core/lib/serializer.js +++ b/packages/concerto-core/lib/serializer.js @@ -26,7 +26,8 @@ const TransactionDeclaration = require('./introspect/transactiondeclaration'); const TypedStack = require('./serializer/typedstack'); const baseDefaultOptions = { - validate: true + validate: true, + ergo: false }; /** @@ -81,6 +82,8 @@ class Serializer { * @param {boolean} [options.deduplicateResources] - Generate $id for resources and * if a resources appears multiple times in the object graph only the first instance is * serialized in full, subsequent instances are replaced with a reference to the $id + * @param {boolean} [options.convertResourcesToId] - Convert resources that + * are specified for relationship fields into their id, false by default. * @return {Object} - The Javascript Object that represents the resource * @throws {Error} - throws an exception if resource is not an instance of * Resource or fails validation. @@ -108,7 +111,9 @@ class Serializer { const generator = new JSONGenerator( options.convertResourcesToRelationships === true, options.permitResourcesForRelationships === true, - options.deduplicateResources === true + options.deduplicateResources === true, + options.convertResourcesToId === true, + options.ergo === true ); parameters.stack.clear(); @@ -173,7 +178,7 @@ class Serializer { parameters.resourceStack = new TypedStack(resource); parameters.modelManager = this.modelManager; parameters.factory = this.factory; - const populator = new JSONPopulator(options.acceptResourcesForRelationships === true); + const populator = new JSONPopulator(options.acceptResourcesForRelationships === true, options.ergo === true); classDeclaration.accept(populator, parameters); // validate the resource against the model diff --git a/packages/concerto-core/lib/serializer/jsongenerator.js b/packages/concerto-core/lib/serializer/jsongenerator.js index 39428ca9a9..40c90c29f0 100644 --- a/packages/concerto-core/lib/serializer/jsongenerator.js +++ b/packages/concerto-core/lib/serializer/jsongenerator.js @@ -45,11 +45,16 @@ class JSONGenerator { * @param {boolean} [deduplicateResources] If resources appear several times * in the object graph only the first instance is serialized, with only the $id * written for subsequent instances, false by default. + * @param {boolean} [convertResourcesToId] Convert resources that + * are specified for relationship fields into their id, false by default. + * @param {boolean} [ergo] target ergo. */ - constructor(convertResourcesToRelationships, permitResourcesForRelationships, deduplicateResources) { + constructor(convertResourcesToRelationships, permitResourcesForRelationships, deduplicateResources, convertResourcesToId, ergo) { this.convertResourcesToRelationships = convertResourcesToRelationships; this.permitResourcesForRelationships = permitResourcesForRelationships; this.deduplicateResources = deduplicateResources; + this.convertResourcesToId = convertResourcesToId; + this.ergo = ergo; } /** @@ -141,8 +146,27 @@ class JSONGenerator { } } result = array; - } else if (field.isPrimitive() || ModelUtil.isEnum(field)) { + } else if (field.isPrimitive()) { result = this.convertToJSON(field, obj); + } else if (ModelUtil.isEnum(field)) { + if (this.ergo) { + // Boxes an enum value to the expected combination of sum types + const enumDeclaration = field.getParent().getModelFile().getType(field.getType()); + const enumName = enumDeclaration.getFullyQualifiedName(); + const properties = enumDeclaration.getProperties(); + let either = { 'left' : obj }; + for(let n=0; n < properties.length; n++) { + const property = properties[n]; + if(property.getName() === obj) { + break; + } else { + either = { 'right' : either }; + } + } + result = { 'type' : [enumName], 'data': either }; + } else { + result = this.convertToJSON(field, obj); + } } else { parameters.stack.push(obj); const classDeclaration = parameters.modelManager.getType(obj.getFullyQualifiedType()); @@ -163,10 +187,20 @@ class JSONGenerator { switch (field.getType()) { case 'DateTime': { - return obj.isUtc() ? obj.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]') : obj.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + if (this.ergo) { + return obj; + } else { + return obj.isUtc() ? obj.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]') : obj.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + } } case 'Integer': - case 'Long': + case 'Long': { + if (this.ergo) { + return { nat: obj }; + } else { + return obj; + } + } case 'Double': case 'Boolean': default: @@ -243,7 +277,11 @@ class JSONGenerator { throw new Error('Did not find a relationship for ' + relationshipDeclaration.getFullyQualifiedTypeName() + ' found ' + relationshipOrResource); } } - return relationshipOrResource.toURI(); + if (this.convertResourcesToId) { + return relationshipOrResource.getIdentifier(); + } else { + return relationshipOrResource.toURI(); + } } } diff --git a/packages/concerto-core/lib/serializer/jsonpopulator.js b/packages/concerto-core/lib/serializer/jsonpopulator.js index d6981a4cc3..a5f0488483 100644 --- a/packages/concerto-core/lib/serializer/jsonpopulator.js +++ b/packages/concerto-core/lib/serializer/jsonpopulator.js @@ -80,9 +80,11 @@ class JSONPopulator { * Constructor. * @param {boolean} [acceptResourcesForRelationships] Permit resources in the * place of relationships, false by default. + * @param {boolean} [ergo] target ergo. */ - constructor(acceptResourcesForRelationships) { + constructor(acceptResourcesForRelationships, ergo) { this.acceptResourcesForRelationships = acceptResourcesForRelationships; + this.ergo = ergo; } /** @@ -221,7 +223,7 @@ class JSONPopulator { break; case 'Integer': case 'Long': - result = parseInt(json); + result = this.ergo ? parseInt(json.nat) : parseInt(json); break; case 'Double': result = parseFloat(json); @@ -232,11 +234,20 @@ class JSONPopulator { case 'String': result = json.toString(); break; - default: + default: { // everything else should be an enumerated value... - result = json; + if (this.ergo) { + // unpack the enum + let current = json.data; + while (!current.left) { + current = current.right; + } + result = current.left; + } else { + result = json; + } + } } - return result; } diff --git a/packages/concerto-core/test/models/wildcards.js b/packages/concerto-core/test/models/wildcards.js index 6c1ef612fb..84aa87603b 100644 --- a/packages/concerto-core/test/models/wildcards.js +++ b/packages/concerto-core/test/models/wildcards.js @@ -27,12 +27,15 @@ describe('Wildcards Model', function () { let factory; let modelManager; let serializer; + let ergoSerializer; beforeEach(() => { modelManager = new ModelManager(); Util.addComposerSystemModels(modelManager); factory = new Factory(modelManager); serializer = new Serializer(factory, modelManager); + ergoSerializer = new Serializer(factory, modelManager); + ergoSerializer.setDefaultOptions({ ergo: true }); const files = [ './test/data/model/dependencies/base/base.cto', './test/data/model/wildcards.cto' @@ -77,6 +80,36 @@ describe('Wildcards Model', function () { resource.person.getFullyQualifiedIdentifier().should.equal('stdlib.base.Person#ALICE_1'); }); + it('should parse a resource using types from a wildcard import (Ergo)', () => { + const json = { + $class: 'org.acme.wildcards.MyAsset', + assetId: '1', + concept: { + $class: 'org.acme.wildcards.MyConcept', + gender: { 'type': 'stdlib.base.Gender', 'data': { 'right' : { 'left': 'FEMALE' } } } + }, + participant: { + $class: 'org.acme.wildcards.MyParticipant', + participantId: '1', + firstName: 'Alice', + lastName: 'A', + contactDetails: { + $class: 'stdlib.base.ContactDetails', + email: 'alice@email.com' + } + }, + person: 'resource:stdlib.base.Person#ALICE_1' + }; + const resource = ergoSerializer.fromJSON(json); + resource.assetId.should.equal('1'); + resource.concept.gender.should.equal('FEMALE'); + resource.participant.participantId.should.equal('1'); + resource.participant.firstName.should.equal('Alice'); + resource.participant.lastName.should.equal('A'); + resource.participant.contactDetails.email.should.equal('alice@email.com'); + resource.person.getFullyQualifiedIdentifier().should.equal('stdlib.base.Person#ALICE_1'); + }); + it('should serialize a resource using types from a wildcard import', () => { const resource = factory.newResource('org.acme.wildcards', 'MyAsset', '1'); resource.assetId = '1'; diff --git a/packages/concerto-core/test/serializer/jsongenerator.js b/packages/concerto-core/test/serializer/jsongenerator.js index 441481670c..84b8e569de 100644 --- a/packages/concerto-core/test/serializer/jsongenerator.js +++ b/packages/concerto-core/test/serializer/jsongenerator.js @@ -30,6 +30,8 @@ describe('JSONGenerator', () => { let modelManager; let factory; let jsonGenerator; + let ergoJsonGenerator; + let ergoJsonGeneratorId; let sandbox; let relationshipDeclaration1; let relationshipDeclaration2; @@ -108,6 +110,8 @@ describe('JSONGenerator', () => { beforeEach(() => { sandbox = sinon.createSandbox(); jsonGenerator = new JSONGenerator(); + ergoJsonGenerator = new JSONGenerator(null,null,null,null,true); + ergoJsonGeneratorId = new JSONGenerator(null,null,null,true,true); }); afterEach(() => { @@ -128,6 +132,7 @@ describe('JSONGenerator', () => { it('should pass through an integer object', () => { jsonGenerator.convertToJSON({ getType: () => { return 'Integer'; } }, 123456).should.equal(123456); + ergoJsonGenerator.convertToJSON({ getType: () => { return 'Integer'; } }, 123456).nat.should.equal(123456); }); it('should pass through a double object', () => { @@ -136,6 +141,7 @@ describe('JSONGenerator', () => { it('should pass through a long object', () => { jsonGenerator.convertToJSON({ getType: () => { return 'Long'; } }, 1234567890).should.equal(1234567890); + ergoJsonGenerator.convertToJSON({ getType: () => { return 'Long'; } }, 1234567890).nat.should.equal(1234567890); }); it('should pass through a string object', () => { @@ -147,10 +153,12 @@ describe('JSONGenerator', () => { it('should convert a date time object to ISOString', () => { let date = Moment.parseZone('Wed, 09 Aug 1995 00:00:00 GMT'); jsonGenerator.convertToJSON({ getType: () => { return 'DateTime'; } }, date).should.equal('1995-08-09T00:00:00.000Z'); + ergoJsonGenerator.convertToJSON({ getType: () => { return 'DateTime'; } }, date).format('YYYY-MM-DDTHH:mm:ss.SSS[Z]').should.equal('1995-08-09T00:00:00.000Z'); }); it('should convert a date time object to ISOString in a different timezone', () => { let date = Moment.parseZone('Wed, 09 Aug 1995 00:00:00 -0500'); jsonGenerator.convertToJSON({ getType: () => { return 'DateTime'; } }, date).should.equal('1995-08-09T00:00:00.000-05:00'); + ergoJsonGenerator.convertToJSON({ getType: () => { return 'DateTime'; } }, date).format('YYYY-MM-DDTHH:mm:ss.SSSZ').should.equal('1995-08-09T00:00:00.000-05:00'); }); it('should pass through a boolean object', () => { @@ -283,6 +291,29 @@ describe('JSONGenerator', () => { jsonGenerator.visitClassDeclaration(relationshipDeclaration4, options); }).should.throw(/Expected a Resource or a Concept/); }); + + it('should generate a relationship (Ergo)', () => { + let relationship = factory.newRelationship('org.acme', 'MyAsset1', 'DOGE_1'); + let options = { + stack: new TypedStack({}), + modelManager: modelManager + }; + options.stack.push(relationship); + let result = ergoJsonGenerator.visitRelationshipDeclaration(relationshipDeclaration1, options); + result.should.be.equal(result, 'resource:org.acme.MyAsset1#DOGE_1'); + }); + + it('should generate a relationship (Ergo with Id)', () => { + let relationship = factory.newRelationship('org.acme', 'MyAsset1', 'DOGE_1'); + let options = { + stack: new TypedStack({}), + modelManager: modelManager + }; + options.stack.push(relationship); + let result = ergoJsonGeneratorId.visitRelationshipDeclaration(relationshipDeclaration1, options); + result.should.be.equal(result, 'DOGE_1'); + }); + }); describe('#getRelationshipText', () => { @@ -354,6 +385,23 @@ describe('JSONGenerator', () => { should.equal(jsonGenerator.visitField(field, parameters), 2); }); + it('should populate if a primitive integer (Ergo)', () => { + let field = { + 'isArray':function(){return false;}, + 'isPrimitive':function(){return true;}, + 'getType':function(){return 'Integer';} + }; + isEnumStub.returns(false); + let primitive = 2; + let parameters = { + stack: new TypedStack({}), + modelManager: modelManager, + seenResources: new Set() + }; + parameters.stack.push(primitive); + should.equal(ergoJsonGenerator.visitField(field, parameters).nat, 2); + }); + it('should populate if a primitive double', () => { let field = { 'isArray':function(){return false;}, @@ -388,6 +436,23 @@ describe('JSONGenerator', () => { should.equal(jsonGenerator.visitField(field, parameters), 1234567890); }); + it('should populate if a primitive Long', () => { + let field = { + 'isArray':function(){return false;}, + 'isPrimitive':function(){return true;}, + 'getType':function(){return 'Long';} + }; + isEnumStub.returns(false); + let primitive = 1234567890; + let parameters = { + stack: new TypedStack({}), + modelManager: modelManager, + seenResources: new Set() + }; + parameters.stack.push(primitive); + should.equal(ergoJsonGenerator.visitField(field, parameters).nat, 1234567890); + }); + it('should populate if a primitive Boolean', () => { let field = { 'isArray':function(){return false;}, @@ -423,6 +488,49 @@ describe('JSONGenerator', () => { should.equal(jsonGenerator.visitField(field, parameters), 'WONGA-1'); }); + it('should populate if an Enum (Ergo)', () => { + let field = { + 'isArray':function(){return false;}, + 'isPrimitive':function(){return false;}, + 'getType':function(){return 'String';}, + 'getParent':function(){ + return { + 'getModelFile':function(){ + return { + 'getType':function(){ + return { + 'getFullyQualifiedName':function(){return 'MyEnum';}, + 'getProperties':function(){return [ + {'getName':function(){ return 'FOO'; }}, + {'getName':function(){ return 'BAR'; }}, + {'getName':function(){ return 'WONGA-1'; }}, + ];} + }; + } + }; + } + }; + } + }; + isEnumStub.returns(true); + + let primitive = 'WONGA-1'; + let parameters = { + stack: new TypedStack({}), + modelManager: modelManager, + seenResources: new Set() + }; + parameters.stack.push(primitive); + let result = ergoJsonGenerator.visitField(field, parameters); + result.should.have.property('type'); + result.should.have.property('data'); + result.type[0].should.equal('MyEnum'); + result.data.should.have.property('right'); + result.data.right.should.have.property('right'); + result.data.right.right.should.have.property('left'); + result.data.right.right.left.should.equal('WONGA-1'); + }); + it('should recurse if an object', () => { let field = { 'getName':function(){return 'vehicle';}, @@ -455,6 +563,38 @@ describe('JSONGenerator', () => { spy.callCount.should.equal(4); // We call it once at the start, then it recurses three times }); + it('should recurse if an object (Ergo)', () => { + let field = { + 'getName':function(){return 'vehicle';}, + 'isArray':function(){return false;}, + 'isPrimitive':function(){return false;}, + 'getParent':function(){return 'vehicle';}, + 'getType':function(){return 'String';} + }; + isEnumStub.returns(false); + + let concept = factory.newConcept('org.acme.sample','Car'); + concept.numberPlate = 'PENGU1N'; + concept.numberOfSeats = '2'; + concept.color = 'GREEN'; + + let parameters = { + stack: new TypedStack({}), + modelManager: modelManager, + seenResources: new Set() + }; + parameters.stack.push(concept); + + let spy = sinon.spy(ergoJsonGenerator, 'visitField'); + + let result = ergoJsonGenerator.visitField(field,parameters); + result.should.deep.equal({ '$class': 'org.acme.sample.Car', + color: 'GREEN', + numberOfSeats: { 'nat' : '2' }, + numberPlate: 'PENGU1N' }); + spy.callCount.should.equal(4); // We call it once at the start, then it recurses three times + }); + it('should populate an array if array contains primitives', () => { let field = { 'isArray':function(){return true;}, @@ -477,7 +617,7 @@ describe('JSONGenerator', () => { it('should populate an array if array contains a Enums', () => { let field = { 'getName':function(){return 'vehicle';}, - 'isArray':function(){return false;}, + 'isArray':function(){return true;}, 'isPrimitive':function(){return false;}, 'getParent':function(){return 'vehicle';}, 'getType':function(){return 'Integer';} diff --git a/packages/concerto-core/test/serializer/jsonpopulator.js b/packages/concerto-core/test/serializer/jsonpopulator.js index 265f2d0a6b..2bf035bb57 100644 --- a/packages/concerto-core/test/serializer/jsonpopulator.js +++ b/packages/concerto-core/test/serializer/jsonpopulator.js @@ -33,6 +33,7 @@ describe('JSONPopulator', () => { let modelManager; let mockFactory; let jsonPopulator; + let ergoJsonPopulator; let sandbox; let assetDeclaration1; let relationshipDeclaration1; @@ -80,6 +81,7 @@ describe('JSONPopulator', () => { sandbox = sinon.createSandbox(); mockFactory = sinon.createStubInstance(Factory); jsonPopulator = new JSONPopulator(); + ergoJsonPopulator = new JSONPopulator(null,true); }); afterEach(() => { @@ -117,6 +119,8 @@ describe('JSONPopulator', () => { field.getType.returns('Integer'); let value = jsonPopulator.convertToObject(field, '32768'); value.should.equal(32768); + value = ergoJsonPopulator.convertToObject(field, {'nat':'32768'}); + value.should.equal(32768); }); it('should convert to integers from numbers', () => { @@ -124,6 +128,8 @@ describe('JSONPopulator', () => { field.getType.returns('Integer'); let value = jsonPopulator.convertToObject(field, 32768); value.should.equal(32768); + value = ergoJsonPopulator.convertToObject(field, {'nat':'32768'}); + value.should.equal(32768); }); it('should convert to longs from strings', () => { @@ -131,6 +137,8 @@ describe('JSONPopulator', () => { field.getType.returns('Long'); let value = jsonPopulator.convertToObject(field, '32768'); value.should.equal(32768); + value = ergoJsonPopulator.convertToObject(field, {'nat':'32768'}); + value.should.equal(32768); }); it('should convert to longs from numbers', () => { @@ -138,6 +146,8 @@ describe('JSONPopulator', () => { field.getType.returns('Long'); let value = jsonPopulator.convertToObject(field, 32768); value.should.equal(32768); + value = ergoJsonPopulator.convertToObject(field, {'nat':'32768'}); + value.should.equal(32768); }); it('should convert to doubles from strings', () => {