From 655eac4682bcc94745e09fd04876f4774d074e8b Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Thu, 26 Aug 2021 15:58:25 +0200 Subject: [PATCH] feat(typescript) : generate interfaces and classes #322 (#323) Signed-off-by: Dan Selman --- .../fromcto/typescript/typescriptvisitor.js | 69 ++++++--- .../fromcto/typescript/typescriptvisitor.js | 138 ++++++++++++------ 2 files changed, 142 insertions(+), 65 deletions(-) diff --git a/packages/concerto-tools/lib/codegen/fromcto/typescript/typescriptvisitor.js b/packages/concerto-tools/lib/codegen/fromcto/typescript/typescriptvisitor.js index be028a8e2c..a4924a08b1 100644 --- a/packages/concerto-tools/lib/codegen/fromcto/typescript/typescriptvisitor.js +++ b/packages/concerto-tools/lib/codegen/fromcto/typescript/typescriptvisitor.js @@ -89,8 +89,11 @@ class TypescriptVisitor { visitModelFile(modelFile, parameters) { parameters.fileWriter.openFile(modelFile.getNamespace() + '.ts'); + parameters.fileWriter.writeLine(0, `// Generated code for namespace: ${modelFile.getNamespace()}`); + // Compute the types we need to import (based on all the types of the properites // as well as all the super types) for all the classes in this model file + parameters.fileWriter.writeLine(0, '\n// imports'); const properties = new Map(); modelFile.getAllDeclarations() .filter(v => !v.isEnum()) @@ -101,6 +104,7 @@ class TypescriptVisitor { if (!properties.has(typeNamespace)) { properties.set(typeNamespace, new Set()); } + properties.get(typeNamespace).add(`I${typeName}`); properties.get(typeNamespace).add(typeName); } @@ -111,6 +115,7 @@ class TypescriptVisitor { if (!properties.has(typeNamespace)) { properties.set(typeNamespace, new Set()); } + properties.get(typeNamespace).add(`I${typeName}`); properties.get(typeNamespace).add(typeName); } }); @@ -126,13 +131,11 @@ class TypescriptVisitor { } }); - parameters.fileWriter.writeLine(0, '// export namespace ' + modelFile.getNamespace() + '{'); - + parameters.fileWriter.writeLine(0, '\n// types'); modelFile.getAllDeclarations().forEach((decl) => { decl.accept(this, parameters); }); - parameters.fileWriter.writeLine(0, '// }'); parameters.fileWriter.closeFile(); return null; @@ -147,13 +150,13 @@ class TypescriptVisitor { */ visitEnumDeclaration(enumDeclaration, parameters) { - parameters.fileWriter.writeLine(1, 'export enum ' + enumDeclaration.getName() + ' {'); + parameters.fileWriter.writeLine(0, 'export enum ' + enumDeclaration.getName() + ' {'); enumDeclaration.getOwnProperties().forEach((property) => { property.accept(this, parameters); }); - parameters.fileWriter.writeLine(1, '}'); + parameters.fileWriter.writeLine(0, '}\n'); return null; } @@ -166,26 +169,42 @@ class TypescriptVisitor { */ visitClassDeclaration(classDeclaration, parameters) { - - let isAbstract = ''; - if (classDeclaration.isAbstract()) { - isAbstract = 'export abstract '; - } else { - isAbstract = 'export '; + let superType = ' '; + if (classDeclaration.getSuperType()) { + superType = ` extends I${ModelUtil.getShortName(classDeclaration.getSuperType())} `; } - let superType = ''; + parameters.fileWriter.writeLine(0, 'export interface I' + classDeclaration.getName() + superType + '{'); + + classDeclaration.getOwnProperties().forEach((property) => { + property.accept(this, parameters); + }); + + parameters.fileWriter.writeLine(0, '}\n'); + + const exportDecl = classDeclaration.isAbstract() ? 'export abstract' : 'export'; + if (classDeclaration.getSuperType()) { - superType = ' extends ' + ModelUtil.getShortName(classDeclaration.getSuperType()); + superType = ` extends ${ModelUtil.getShortName(classDeclaration.getSuperType())} `; } - parameters.fileWriter.writeLine(1, isAbstract + 'class ' + classDeclaration.getName() + superType + ' {'); - + parameters.fileWriter.writeLine(0, `${exportDecl} class ${classDeclaration.getName()}${superType}implements I${classDeclaration.getName()} {`); + parameters.fileWriter.writeLine(1, 'public static $class: string'); + parameters.useDefiniteAssignment = true; classDeclaration.getOwnProperties().forEach((property) => { property.accept(this, parameters); }); + parameters.useDefiniteAssignment = false; + parameters.fileWriter.writeLine(1,`public constructor(data: I${classDeclaration.getName()}) {`); + if(classDeclaration.getSuperType()) { + parameters.fileWriter.writeLine(2, 'super(data);'); + } + parameters.fileWriter.writeLine(2, 'Object.assign(this, data);'); + parameters.fileWriter.writeLine(2, `${classDeclaration.getName()}.$class = '${classDeclaration.getFullyQualifiedName()}'`); parameters.fileWriter.writeLine(1, '}'); + parameters.fileWriter.writeLine(0, '}\n'); + return null; } @@ -203,7 +222,12 @@ class TypescriptVisitor { array = '[]'; } - parameters.fileWriter.writeLine(2, field.getName() + ': ' + this.toTsType(field.getType()) + array + ';'); + const isEnumRef = field.isPrimitive() ? false + : field.getParent().getModelFile().getModelManager().getType(field.getFullyQualifiedTypeName()).isEnum(); + + const assignment = parameters.useDefiniteAssignment ? '!' : ''; + + parameters.fileWriter.writeLine(1, field.getName() + assignment + ': ' + this.toTsType(field.getType(), !isEnumRef) + array + ';'); return null; } @@ -215,7 +239,7 @@ class TypescriptVisitor { * @private */ visitEnumValueDeclaration(enumValueDeclaration, parameters) { - parameters.fileWriter.writeLine(2, enumValueDeclaration.getName() + ','); + parameters.fileWriter.writeLine(1, enumValueDeclaration.getName() + ','); return null; } @@ -233,8 +257,10 @@ class TypescriptVisitor { array = '[]'; } - // we export all relationships by capitalizing them - parameters.fileWriter.writeLine(2, relationship.getName() + ': ' + this.toTsType(relationship.getType()) + array + ';'); + const assignment = parameters.useDefiniteAssignment ? '!' : ''; + + // we export all relationships + parameters.fileWriter.writeLine(1, relationship.getName() + assignment + ': ' + this.toTsType(relationship.getType(), true) + array + ';'); return null; } @@ -242,10 +268,11 @@ class TypescriptVisitor { * Converts a Concerto type to a Typescript type. Primitive types are converted * everything else is passed through unchanged. * @param {string} type - the concerto type + * @param {boolean} useInterface - whether to use an interface type * @return {string} the corresponding type in Typescript * @private */ - toTsType(type) { + toTsType(type, useInterface) { switch (type) { case 'DateTime': return 'Date'; @@ -260,7 +287,7 @@ class TypescriptVisitor { case 'Integer': return 'number'; default: - return type; + return useInterface ? `I${type}` : type; } } } diff --git a/packages/concerto-tools/test/codegen/fromcto/typescript/typescriptvisitor.js b/packages/concerto-tools/test/codegen/fromcto/typescript/typescriptvisitor.js index a4d9c44997..8660d03848 100644 --- a/packages/concerto-tools/test/codegen/fromcto/typescript/typescriptvisitor.js +++ b/packages/concerto-tools/test/codegen/fromcto/typescript/typescriptvisitor.js @@ -223,11 +223,12 @@ describe('TypescriptVisitor', function () { typescriptVisitor.visitModelFile(mockModelFile, param); param.fileWriter.openFile.withArgs('org.acme.ts').calledOnce.should.be.ok; - param.fileWriter.writeLine.callCount.should.deep.equal(4); - param.fileWriter.writeLine.getCall(0).args.should.deep.equal([0, 'import {Property1} from \'./org.org1\';']); - param.fileWriter.writeLine.getCall(1).args.should.deep.equal([0, 'import {Parent} from \'./super\';']); - param.fileWriter.writeLine.getCall(2).args.should.deep.equal([0, '// export namespace org.acme{']); - param.fileWriter.writeLine.getCall(3).args.should.deep.equal([0, '// }']); + param.fileWriter.writeLine.callCount.should.deep.equal(5); + param.fileWriter.writeLine.getCall(0).args.should.deep.equal([0, '// Generated code for namespace: org.acme']); + param.fileWriter.writeLine.getCall(1).args.should.deep.equal([0, '\n// imports']); + param.fileWriter.writeLine.getCall(2).args.should.deep.equal([0, 'import {IProperty1,Property1} from \'./org.org1\';']); + param.fileWriter.writeLine.getCall(3).args.should.deep.equal([0, 'import {IParent,Parent} from \'./super\';']); + param.fileWriter.writeLine.getCall(4).args.should.deep.equal([0, '\n// types']); param.fileWriter.closeFile.calledOnce.should.be.ok; acceptSpy.withArgs(typescriptVisitor, param).calledThrice.should.be.ok; @@ -277,7 +278,7 @@ describe('TypescriptVisitor', function () { let mockModelFile = sinon.createStubInstance(ModelFile); mockModelFile._isModelFile = true; - mockModelFile.getNamespace.returns('org.acme.Person'); + mockModelFile.getNamespace.returns('org.acme'); mockModelFile.getAllDeclarations.returns([ mockEnum, mockClassDeclaration @@ -291,11 +292,12 @@ describe('TypescriptVisitor', function () { typescriptVisitor.visitModelFile(mockModelFile, param); - param.fileWriter.openFile.withArgs('org.acme.Person.ts').calledOnce.should.be.ok; - param.fileWriter.writeLine.callCount.should.deep.equal(3); - param.fileWriter.writeLine.getCall(0).args.should.deep.equal([0, 'import {Property1,Property3} from \'./org.org1\';']); - param.fileWriter.writeLine.getCall(1).args.should.deep.equal([0, '// export namespace org.acme.Person{']); - param.fileWriter.writeLine.getCall(2).args.should.deep.equal([0, '// }']); + param.fileWriter.openFile.withArgs('org.acme.ts').calledOnce.should.be.ok; + param.fileWriter.writeLine.callCount.should.deep.equal(4); + param.fileWriter.writeLine.getCall(0).args.should.deep.equal([0, '// Generated code for namespace: org.acme']); + param.fileWriter.writeLine.getCall(1).args.should.deep.equal([0, '\n// imports']); + param.fileWriter.writeLine.getCall(2).args.should.deep.equal([0, 'import {IProperty1,Property1,IProperty3,Property3} from \'./org.org1\';']); + param.fileWriter.writeLine.getCall(3).args.should.deep.equal([0, '\n// types']); param.fileWriter.closeFile.calledOnce.should.be.ok; acceptSpy.withArgs(typescriptVisitor, param).calledTwice.should.be.ok; @@ -311,7 +313,7 @@ describe('TypescriptVisitor', function () { }; let mockEnumDeclaration = sinon.createStubInstance(EnumDeclaration); - mockEnumDeclaration._isEnumDeclaration = true; + mockEnumDeclaration.isEnum.returns(true); mockEnumDeclaration.getName.returns('Bob'); mockEnumDeclaration.getOwnProperties.returns([{ accept: acceptSpy @@ -323,8 +325,8 @@ describe('TypescriptVisitor', function () { typescriptVisitor.visitEnumDeclaration(mockEnumDeclaration, param); param.fileWriter.writeLine.callCount.should.deep.equal(2); - param.fileWriter.writeLine.withArgs(1, 'export enum Bob {').calledOnce.should.be.ok; - param.fileWriter.writeLine.withArgs(1, '}').calledOnce.should.be.ok; + param.fileWriter.writeLine.withArgs(0, 'export enum Bob {').calledOnce.should.be.ok; + param.fileWriter.writeLine.withArgs(0, '}\n').calledOnce.should.be.ok; acceptSpy.withArgs(typescriptVisitor, param).calledTwice.should.be.ok; }); @@ -352,10 +354,16 @@ describe('TypescriptVisitor', function () { typescriptVisitor.visitClassDeclaration(mockClassDeclaration, param); - param.fileWriter.writeLine.callCount.should.deep.equal(2); - param.fileWriter.writeLine.withArgs(1, 'export class Bob {').calledOnce.should.be.ok; - param.fileWriter.writeLine.withArgs(1, '}').calledOnce.should.be.ok; - acceptSpy.withArgs(typescriptVisitor, param).calledTwice.should.be.ok; + param.fileWriter.writeLine.callCount.should.deep.equal(9); + param.fileWriter.writeLine.getCall(0).args.should.deep.equal([0, 'export interface IBob {']); + param.fileWriter.writeLine.getCall(1).args.should.deep.equal([0, '}\n']); + param.fileWriter.writeLine.getCall(2).args.should.deep.equal([0, 'export class Bob implements IBob {']); + param.fileWriter.writeLine.getCall(3).args.should.deep.equal([1, 'public static $class: string']); + param.fileWriter.writeLine.getCall(4).args.should.deep.equal([1, 'public constructor(data: IBob) {']); + param.fileWriter.writeLine.getCall(5).args.should.deep.equal([2, 'Object.assign(this, data);']); + param.fileWriter.writeLine.getCall(6).args.should.deep.equal([2, 'Bob.$class = \'undefined\'']); + param.fileWriter.writeLine.getCall(7).args.should.deep.equal([1, '}']); + param.fileWriter.writeLine.getCall(8).args.should.deep.equal([0, '}\n']); }); it('should write the class opening and close with abstract and super type', () => { let acceptSpy = sinon.spy(); @@ -374,10 +382,17 @@ describe('TypescriptVisitor', function () { typescriptVisitor.visitClassDeclaration(mockClassDeclaration, param); - param.fileWriter.writeLine.callCount.should.deep.equal(2); - param.fileWriter.writeLine.withArgs(1, 'export abstract class Bob extends Person {').calledOnce.should.be.ok; - param.fileWriter.writeLine.withArgs(1, '}').calledOnce.should.be.ok; - acceptSpy.withArgs(typescriptVisitor, param).calledTwice.should.be.ok; + param.fileWriter.writeLine.callCount.should.deep.equal(10); + param.fileWriter.writeLine.getCall(0).args.should.deep.equal([0, 'export interface IBob extends IPerson {']); + param.fileWriter.writeLine.getCall(1).args.should.deep.equal([0, '}\n']); + param.fileWriter.writeLine.getCall(2).args.should.deep.equal([0, 'export abstract class Bob extends Person implements IBob {']); + param.fileWriter.writeLine.getCall(3).args.should.deep.equal([1, 'public static $class: string']); + param.fileWriter.writeLine.getCall(4).args.should.deep.equal([1, 'public constructor(data: IBob) {']); + param.fileWriter.writeLine.getCall(5).args.should.deep.equal([2, 'super(data);']); + param.fileWriter.writeLine.getCall(6).args.should.deep.equal([2, 'Object.assign(this, data);']); + param.fileWriter.writeLine.getCall(7).args.should.deep.equal([2, 'Bob.$class = \'undefined\'']); + param.fileWriter.writeLine.getCall(8).args.should.deep.equal([1, '}']); + param.fileWriter.writeLine.getCall(9).args.should.deep.equal([0, '}\n']); }); }); @@ -388,33 +403,66 @@ describe('TypescriptVisitor', function () { fileWriter: mockFileWriter }; }); - it('should write a line for field name and type', () => { - let mockField = sinon.createStubInstance(Field); - mockField._isField = true; + it('should write a line for primitive field name and type', () => { + const mockField = sinon.createStubInstance(Field); + mockField.isPrimitive.returns(false); + mockField.getName.returns('name'); + mockField.getType.returns('String'); + mockField.isPrimitive.returns(true); + typescriptVisitor.visitField(mockField, param); + param.fileWriter.writeLine.withArgs(1, 'name: string;').calledOnce.should.be.ok; + }); + it('should write a line for primitive field name and type using definite assignment', () => { + const mockField = sinon.createStubInstance(Field); + mockField.isPrimitive.returns(false); + mockField.getName.returns('name'); + mockField.getType.returns('String'); + mockField.isPrimitive.returns(true); + param.useDefiniteAssignment = true; + typescriptVisitor.visitField(mockField, param); + param.useDefiniteAssignment = false; + param.fileWriter.writeLine.withArgs(1, 'name!: string;').calledOnce.should.be.ok; + }); + + it('should convert classes to interfaces', () => { + const mockField = sinon.createStubInstance(Field); + mockField.isPrimitive.returns(false); mockField.getName.returns('Bob'); mockField.getType.returns('Person'); - let mockToType = sinon.stub(typescriptVisitor, 'toTsType'); - mockToType.withArgs('Person').returns('Human'); + const mockModelManager = sinon.createStubInstance(ModelManager); + const mockModelFile = sinon.createStubInstance(ModelFile); + const mockClassDeclaration = sinon.createStubInstance(ClassDeclaration); + mockModelManager.getType.returns(mockClassDeclaration); + mockClassDeclaration.isEnum.returns(false); + mockModelFile.getModelManager.returns(mockModelManager); + mockClassDeclaration.getModelFile.returns(mockModelFile); + mockField.getParent.returns(mockClassDeclaration); typescriptVisitor.visitField(mockField, param); - param.fileWriter.writeLine.withArgs(2, 'Bob: Human;').calledOnce.should.be.ok; + param.fileWriter.writeLine.withArgs(1, 'Bob: IPerson;').calledOnce.should.be.ok; }); it('should write a line for field name and type thats an array', () => { - let mockField = sinon.createStubInstance(Field); - mockField._isField = true; + const mockField = sinon.createStubInstance(Field); + mockField.isPrimitive.returns(false); mockField.getName.returns('Bob'); mockField.getType.returns('Person'); mockField.isArray.returns(true); - let mockToType = sinon.stub(typescriptVisitor, 'toTsType'); - mockToType.withArgs('Person').returns('Human'); + const mockModelManager = sinon.createStubInstance(ModelManager); + const mockModelFile = sinon.createStubInstance(ModelFile); + const mockClassDeclaration = sinon.createStubInstance(ClassDeclaration); + mockModelManager.getType.returns(mockClassDeclaration); + mockClassDeclaration.isEnum.returns(false); + mockModelFile.getModelManager.returns(mockModelManager); + mockClassDeclaration.getModelFile.returns(mockModelFile); + mockField.getParent.returns(mockClassDeclaration); typescriptVisitor.visitField(mockField, param); - param.fileWriter.writeLine.withArgs(2, 'Bob: Human[];').calledOnce.should.be.ok; + param.fileWriter.writeLine.withArgs(1, 'Bob: IPerson[];').calledOnce.should.be.ok; }); }); @@ -430,7 +478,7 @@ describe('TypescriptVisitor', function () { typescriptVisitor.visitEnumValueDeclaration(mockEnumValueDeclaration, param); - param.fileWriter.writeLine.withArgs(2, 'Bob,').calledOnce.should.be.ok; + param.fileWriter.writeLine.withArgs(1, 'Bob,').calledOnce.should.be.ok; }); }); @@ -446,13 +494,19 @@ describe('TypescriptVisitor', function () { mockRelationship._isRelationshipDeclaration = true; mockRelationship.getName.returns('Bob'); mockRelationship.getType.returns('Person'); - - let mockToType = sinon.stub(typescriptVisitor, 'toTsType'); - mockToType.withArgs('Person').returns('Human'); - typescriptVisitor.visitRelationship(mockRelationship, param); - param.fileWriter.writeLine.withArgs(2, 'Bob: Human;').calledOnce.should.be.ok; + param.fileWriter.writeLine.withArgs(1, 'Bob: IPerson;').calledOnce.should.be.ok; + }); + it('should write a line for field name and type using definite assignment', () => { + let mockRelationship = sinon.createStubInstance(RelationshipDeclaration); + mockRelationship._isRelationshipDeclaration = true; + mockRelationship.getName.returns('Bob'); + mockRelationship.getType.returns('Person'); + param.useDefiniteAssignment = true; + typescriptVisitor.visitRelationship(mockRelationship, param); + param.useDefiniteAssignment = false; + param.fileWriter.writeLine.withArgs(1, 'Bob!: IPerson;').calledOnce.should.be.ok; }); it('should write a line for field name and type thats an array', () => { @@ -461,13 +515,9 @@ describe('TypescriptVisitor', function () { mockField.getName.returns('Bob'); mockField.getType.returns('Person'); mockField.isArray.returns(true); - - let mockToType = sinon.stub(typescriptVisitor, 'toTsType'); - mockToType.withArgs('Person').returns('Human'); - typescriptVisitor.visitRelationship(mockField, param); - param.fileWriter.writeLine.withArgs(2, 'Bob: Human[];').calledOnce.should.be.ok; + param.fileWriter.writeLine.withArgs(1, 'Bob: IPerson[];').calledOnce.should.be.ok; }); });