diff --git a/Dockerfile b/Dockerfile index 804f4f454e..683be21c71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,9 @@ FROM openjdk:16.0.1-jdk-slim-buster RUN apt-get update -yq \ && apt-get install -yq curl \ && curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \ - && apt-get install -yq nodejs + && apt-get install -yq nodejs \ + && curl -fsSL https://golang.org/dl/go1.16.5.linux-amd64.tar.gz | tar -C /usr/local -xz +ENV PATH="${PATH}:/usr/local/go/bin" COPY package-lock.json . RUN npm install diff --git a/docs/generators.md b/docs/generators.md index 59980a68ce..1a56275472 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -7,6 +7,7 @@ - [JavaScript](../src/generators/javascript/JavaScriptGenerator.ts), - [TypeScript](../src/generators/typescript/TypeScriptGenerator.ts), - [Java](../src/generators/java/JavaGenerator.ts). +- [Go](../src/generators/go/GoGenerator.ts). ## Generator's options diff --git a/docs/presets.md b/docs/presets.md index d2fead0e20..c3d9136343 100644 --- a/docs/presets.md +++ b/docs/presets.md @@ -193,3 +193,11 @@ Below is a list of supported languages with their model types and corresponding #### **Type** There are no additional methods. + +### Go + +#### **Struct** + +| Method | Description | Additional arguments | +|---|---|---| +| `field` | A method to extend rendered given field. | `fieldName` as a name of a given field, `field` object as a [`CommonModel`](../src/models/CommonModel.ts) instance. | diff --git a/sonar-project.properties b/sonar-project.properties index 8628ad6f42..7a5e9f8992 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,2 +1,2 @@ # Disable specific duplicate code since it would introduce more complexity to reduce it. -sonar.cpd.exclusions=src/generators/javascript/renderers/ClassRenderer.ts,src/generators/java/renderers/ClassRenderer.ts,src/generators/typescript/renderers/ClassRenderer.ts,src/generators/javascript/JavaScriptRenderer.ts,src/generators/typescript/renderers/EnumRenderer.ts,src/generators/java/renderers/EnumRenderer.ts +sonar.cpd.exclusions=src/generators/javascript/renderers/ClassRenderer.ts,src/generators/java/renderers/ClassRenderer.ts,src/generators/typescript/renderers/ClassRenderer.ts,src/generators/javascript/JavaScriptRenderer.ts,src/generators/java/renderers/ClassRenderer.ts,src/generators/typescript/TypeScriptRenderer.ts,src/generators/typescript/renderers/EnumRenderer.ts,src/generators/java/renderers/EnumRenderer.ts diff --git a/src/generators/go/GoGenerator.ts b/src/generators/go/GoGenerator.ts index 929439740b..6bcaeccc00 100644 --- a/src/generators/go/GoGenerator.ts +++ b/src/generators/go/GoGenerator.ts @@ -7,6 +7,7 @@ import { CommonModel, CommonInputModel, RenderOutput } from '../../models'; import { TypeHelpers, ModelKind } from '../../helpers'; import { GoPreset, GO_DEFAULT_PRESET } from './GoPreset'; import { StructRenderer } from './renderers/StructRenderer'; +import { EnumRenderer } from './renderers/EnumRenderer'; export type GoOptions = CommonGeneratorOptions @@ -27,12 +28,25 @@ export class GoGenerator extends AbstractGenerator { render(model: CommonModel, inputModel: CommonInputModel): Promise { const kind = TypeHelpers.extractKind(model); - if (kind === ModelKind.OBJECT) { + switch (kind) { + case ModelKind.OBJECT: { return this.renderStruct(model, inputModel); } + case ModelKind.ENUM: { + return this.renderEnum(model, inputModel); + } + } + return Promise.resolve(RenderOutput.toRenderOutput({ result: '', dependencies: [] })); } + async renderEnum(model: CommonModel, inputModel: CommonInputModel): Promise { + const presets = this.getPresets('enum'); + const renderer = new EnumRenderer(this.options, presets, model, inputModel); + const result = await renderer.runSelfPreset(); + return RenderOutput.toRenderOutput({ result, dependencies: renderer.dependencies }); + } + async renderStruct(model: CommonModel, inputModel: CommonInputModel): Promise { const presets = this.getPresets('struct'); const renderer = new StructRenderer(this.options, presets, model, inputModel); diff --git a/src/generators/go/GoPreset.ts b/src/generators/go/GoPreset.ts index 426e8a2621..df05957493 100644 --- a/src/generators/go/GoPreset.ts +++ b/src/generators/go/GoPreset.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/ban-types */ import { AbstractRenderer } from '../AbstractRenderer'; -import { Preset, CommonModel, CommonPreset, PresetArgs } from '../../models'; +import { Preset, CommonModel, CommonPreset, PresetArgs, EnumPreset } from '../../models'; import { StructRenderer, GO_DEFAULT_STRUCT_PRESET } from './renderers/StructRenderer'; +import { EnumRenderer, GO_DEFAULT_ENUM_PRESET } from './renderers/EnumRenderer'; export interface FieldArgs { fieldName: string; @@ -9,16 +10,15 @@ export interface FieldArgs { } export interface StructPreset extends CommonPreset { - ctor?: (args: PresetArgs) => Promise | string; field?: (args: PresetArgs & FieldArgs) => Promise | string; - getter?: (args: PresetArgs & FieldArgs) => Promise | string; - setter?: (args: PresetArgs & FieldArgs) => Promise | string; } export type GoPreset = Preset<{ struct: StructPreset; + enum: EnumPreset }>; export const GO_DEFAULT_PRESET: GoPreset = { struct: GO_DEFAULT_STRUCT_PRESET, + enum: GO_DEFAULT_ENUM_PRESET, }; diff --git a/src/generators/go/GoRenderer.ts b/src/generators/go/GoRenderer.ts index daa55caef6..012286dcb0 100644 --- a/src/generators/go/GoRenderer.ts +++ b/src/generators/go/GoRenderer.ts @@ -37,7 +37,7 @@ export abstract class GoRenderer extends AbstractRenderer { renderType(model: CommonModel): string { if (model.$ref !== undefined) { - return FormatHelpers.toPascalCase(model.$ref, { transform: pascalCaseTransformMerge }); + return `*${FormatHelpers.toPascalCase(model.$ref, { transform: pascalCaseTransformMerge })}`; } if (Array.isArray(model.type)) { @@ -47,6 +47,11 @@ export abstract class GoRenderer extends AbstractRenderer { return this.toGoType(model.type, model); } + renderComments(lines: string | string[]): string { + lines = FormatHelpers.breakLines(lines); + return lines.map(line => `// ${line}`).join('\n'); + } + /* eslint-disable sonarjs/no-duplicate-string */ toGoType(type: string | undefined, model: CommonModel): string { if (type === undefined) { diff --git a/src/generators/go/renderers/EnumRenderer.ts b/src/generators/go/renderers/EnumRenderer.ts new file mode 100644 index 0000000000..ff5202e4b4 --- /dev/null +++ b/src/generators/go/renderers/EnumRenderer.ts @@ -0,0 +1,38 @@ +import { GoRenderer } from '../GoRenderer'; +import { EnumPreset, CommonModel } from '../../../models'; +import { FormatHelpers } from '../../../helpers'; + +/** + * Renderer for Go's `enum` type + * + * @extends GoRenderer + */ +export class EnumRenderer extends GoRenderer { + public defaultSelf(): string { + const formattedName = this.model.$id && FormatHelpers.toPascalCase(this.model.$id); + const type = this.enumType(this.model); + const doc = formattedName && this.renderCommentForEnumType(formattedName, type); + + return `${doc} +type ${formattedName} ${type}`; + } + + enumType(model: CommonModel): string { + if (this.model.type === undefined || Array.isArray(this.model.type)) { + return 'interface{}'; + } + + return this.toGoType(this.model.type, model); + } + + renderCommentForEnumType(name: string, type: string): string { + const globalType = type === 'interface{}' ? 'mixed types' : type; + return this.renderComments(`${name} represents an enum of ${globalType}.`); + } +} + +export const GO_DEFAULT_ENUM_PRESET: EnumPreset = { + self({ renderer }) { + return renderer.defaultSelf(); + }, +}; diff --git a/src/generators/go/renderers/StructRenderer.ts b/src/generators/go/renderers/StructRenderer.ts index 0695be161a..0cde00fc4c 100644 --- a/src/generators/go/renderers/StructRenderer.ts +++ b/src/generators/go/renderers/StructRenderer.ts @@ -23,27 +23,13 @@ type ${formattedName} struct { ${this.indent(this.renderBlock(content, 2))} }`; } - - renderComments(lines: string | string[]): string { - lines = FormatHelpers.breakLines(lines); - return lines.map(line => `// ${line}`).join('\n'); - } } export const GO_DEFAULT_STRUCT_PRESET: StructPreset = { self({ renderer }) { return renderer.defaultSelf(); }, - ctor() { - return 'thisShoulBeAConstructor'; - }, field({ fieldName, field, renderer }) { return `${FormatHelpers.toPascalCase(fieldName, { transform: pascalCaseTransformMerge }) } ${ renderer.renderType(field)}`; }, - getter() { - return 'getterFunc'; - }, - setter() { - return 'setterFunc'; - }, }; diff --git a/src/generators/index.ts b/src/generators/index.ts index 73a2267fe6..7516242106 100644 --- a/src/generators/index.ts +++ b/src/generators/index.ts @@ -3,3 +3,4 @@ export * from './AbstractRenderer'; export * from './java'; export * from './javascript'; export * from './typescript'; +export * from './go'; diff --git a/test/blackbox/AsyncAPI.spec.ts b/test/blackbox/AsyncAPI.spec.ts index 979f6ddf42..17b47c1aec 100644 --- a/test/blackbox/AsyncAPI.spec.ts +++ b/test/blackbox/AsyncAPI.spec.ts @@ -1,29 +1,19 @@ import * as fs from 'fs'; import * as path from 'path'; -import { TypeScriptGenerator, JavaGenerator, JavaScriptGenerator } from '../../src'; +import { TypeScriptGenerator, JavaGenerator, JavaScriptGenerator, GoGenerator } from '../../src'; describe('AsyncAPI JSON Schema file', () => { - test('should be generated in TypeScript', async () => { - const inputSchemaString = fs.readFileSync(path.resolve(__dirname, './docs/AsyncAPI_2_0_0.json'), 'utf8'); - const inputSchema = JSON.parse(inputSchemaString); - const generator = new TypeScriptGenerator(); - const generatedContent = await generator.generate(inputSchema); - expect(generatedContent).not.toBeUndefined(); - expect(generatedContent.length).toBeGreaterThan(1); - }); - test('should be generated in Java', async () => { - const inputSchemaString = fs.readFileSync(path.resolve(__dirname, './docs/AsyncAPI_2_0_0.json'), 'utf8'); - const inputSchema = JSON.parse(inputSchemaString); - const generator = new JavaGenerator(); - const generatedContent = await generator.generate(inputSchema); - expect(generatedContent).not.toBeUndefined(); - expect(generatedContent.length).toBeGreaterThan(1); - }); - test('should be generated in JavaScript', async () => { - const inputSchemaString = fs.readFileSync(path.resolve(__dirname, './docs/AsyncAPI_2_0_0.json'), 'utf8'); - const inputSchema = JSON.parse(inputSchemaString); - const generator = new JavaScriptGenerator(); - const generatedContent = await generator.generate(inputSchema); - expect(generatedContent).not.toBeUndefined(); - expect(generatedContent.length).toBeGreaterThan(1); + describe.each([ + ['TypeScript', new TypeScriptGenerator()], + ['Java', new JavaGenerator()], + ['Javascript', new JavaScriptGenerator()], + ['Go', new GoGenerator()], + ])('code generated in %s', (_, generator) => { + test('should not be empty', async () => { + const inputSchemaString = fs.readFileSync(path.resolve(__dirname, './docs/AsyncAPI_2_0_0.json'), 'utf8'); + const inputSchema = JSON.parse(inputSchemaString); + const generatedContent = await generator.generate(inputSchema); + expect(generatedContent).not.toBeUndefined(); + expect(generatedContent.length).toBeGreaterThan(1); + }); }); }); diff --git a/test/blackbox/Dummy.spec.ts b/test/blackbox/Dummy.spec.ts index 22236828cd..d50715ef67 100644 --- a/test/blackbox/Dummy.spec.ts +++ b/test/blackbox/Dummy.spec.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { TypeScriptGenerator, JavaGenerator, JavaScriptGenerator } from '../../src'; +import { TypeScriptGenerator, JavaGenerator, JavaScriptGenerator, GoGenerator } from '../../src'; import { execCommand, generateModels, renderModels } from './utils/Utils'; import { renderJavaModelsToSeparateFiles } from './utils/Utils'; const fileToGenerate = path.resolve(__dirname, './docs/dummy.json'); @@ -53,4 +53,16 @@ describe('Dummy JSON Schema file', () => { await execCommand(transpileAndRunCommand); }); }); + + describe('should be able to generate Go', () => { + test('struct', async () => { + const generator = new GoGenerator(); + const generatedModels = await generateModels(fileToGenerate, generator); + expect(generatedModels).not.toHaveLength(0); + const renderOutputPath = path.resolve(__dirname, './output/go/struct/main.go'); + await renderModels(generatedModels, renderOutputPath, ['package main\n', 'func main() {}']); + const compileCommand = `go build -o ${renderOutputPath.replace('.go', '')} ${renderOutputPath}`; + await execCommand(compileCommand); + }); + }); }); diff --git a/test/blackbox/utils/Utils.ts b/test/blackbox/utils/Utils.ts index 06f1e1b937..30c10e7125 100644 --- a/test/blackbox/utils/Utils.ts +++ b/test/blackbox/utils/Utils.ts @@ -59,13 +59,15 @@ ${outputModel.result} * @param models to write to file * @param outputPath path to output */ -export async function renderModels(generatedModels: OutputModel[], outputPath: string): Promise { +export async function renderModels(generatedModels: OutputModel[], outputPath: string, headers?: string[]): Promise { const outputDir = path.resolve(__dirname, path.dirname(outputPath)); await fs.rm(outputDir, { recursive: true, force: true }); await fs.mkdir(outputDir, { recursive: true }); const output = generatedModels.map((model) => { return model.result; }); + + const stringOutput = headers ? `${headers.join('\n')}\n\n${output.join('\n')}` : output.join('\n'); const outputFilePath = path.resolve(__dirname, outputPath); - await fs.writeFile(outputFilePath, output.join('\n')); + await fs.writeFile(outputFilePath, stringOutput); } diff --git a/test/generators/go/GoGenerator.spec.ts b/test/generators/go/GoGenerator.spec.ts index aa4288c440..695f9bcd5e 100644 --- a/test/generators/go/GoGenerator.spec.ts +++ b/test/generators/go/GoGenerator.spec.ts @@ -1,4 +1,4 @@ -import { GoGenerator } from '../../../src/generators/go/GoGenerator'; +import { GoGenerator } from '../../../src/generators'; describe('GoGenerator', () => { let generator: GoGenerator; @@ -75,4 +75,70 @@ type CustomStruct struct { expect(structModel.result).toEqual(expected); expect(structModel.dependencies).toEqual([]); }); + + describe.each([ + { + name: 'with enums sharing same type', + doc: { + $id: 'States', + type: 'string', + enum: ['Texas', 'Alabama', 'California'], + }, + expected: `// States represents an enum of string. +type States string`, + }, + { + name: 'with enums of mixed types', + doc: { + $id: 'Things', + enum: ['Texas', 1, false], + }, + expected: `// Things represents an enum of mixed types. +type Things interface{}`, + }, + ])('should render `enum` type $name', ({doc, expected}) => { + test('should not be empty', async () => { + const inputModel = await generator.process(doc); + const model = inputModel.models[doc.$id]; + + let enumModel = await generator.render(model, inputModel); + expect(enumModel.result).toEqual(expected); + expect(enumModel.dependencies).toEqual([]); + + enumModel = await generator.renderEnum(model, inputModel); + expect(enumModel.result).toEqual(expected); + expect(enumModel.dependencies).toEqual([]); + }); + }); + + test('should work custom preset for `enum` type', async () => { + const doc = { + $id: 'CustomEnum', + type: 'string', + enum: ['Texas', 'Alabama', 'California'], + }; + const expected = `// CustomEnum represents an enum of string. +type CustomEnum string`; + + generator = new GoGenerator({ presets: [ + { + enum: { + self({ content }) { + return content; + }, + } + } + ] }); + + const inputModel = await generator.process(doc); + const model = inputModel.models['CustomEnum']; + + let enumModel = await generator.render(model, inputModel); + expect(enumModel.result).toEqual(expected); + expect(enumModel.dependencies).toEqual([]); + + enumModel = await generator.renderEnum(model, inputModel); + expect(enumModel.result).toEqual(expected); + expect(enumModel.dependencies).toEqual([]); + }); }); diff --git a/test/generators/go/GoRenderer.spec.ts b/test/generators/go/GoRenderer.spec.ts index 7fd889b8c5..e496875f25 100644 --- a/test/generators/go/GoRenderer.spec.ts +++ b/test/generators/go/GoRenderer.spec.ts @@ -42,7 +42,7 @@ describe('GoRenderer', () => { test('Should render refs with pascal case (no _ prefix before numbers)', () => { const model = new CommonModel(); model.$ref = ''; - expect(renderer.renderType(model)).toEqual('AnonymousSchema1'); + expect(renderer.renderType(model)).toEqual('*AnonymousSchema1'); }); test('Should render union types with one type as slice of that type', () => { const model = CommonModel.toCommonModel({ type: ['number'] });