Skip to content

Commit a345e0a

Browse files
authored
feat: add basic struct generation for Go (#267)
1 parent b9b7294 commit a345e0a

File tree

7 files changed

+329
-0
lines changed

7 files changed

+329
-0
lines changed

src/generators/go/GoGenerator.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
AbstractGenerator,
3+
CommonGeneratorOptions,
4+
defaultGeneratorOptions,
5+
} from '../AbstractGenerator';
6+
import { CommonModel, CommonInputModel, RenderOutput } from '../../models';
7+
import { TypeHelpers, ModelKind } from '../../helpers';
8+
import { GoPreset, GO_DEFAULT_PRESET } from './GoPreset';
9+
import { StructRenderer } from './renderers/StructRenderer';
10+
11+
export type GoOptions = CommonGeneratorOptions<GoPreset>
12+
13+
/**
14+
* Generator for Go
15+
*/
16+
export class GoGenerator extends AbstractGenerator<GoOptions> {
17+
static defaultOptions: GoOptions = {
18+
...defaultGeneratorOptions,
19+
defaultPreset: GO_DEFAULT_PRESET,
20+
};
21+
22+
constructor(
23+
options: GoOptions = GoGenerator.defaultOptions,
24+
) {
25+
super('Go', GoGenerator.defaultOptions, options);
26+
}
27+
28+
render(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
29+
const kind = TypeHelpers.extractKind(model);
30+
if (kind === ModelKind.OBJECT) {
31+
return this.renderStruct(model, inputModel);
32+
}
33+
return Promise.resolve(RenderOutput.toRenderOutput({ result: '', dependencies: [] }));
34+
}
35+
36+
async renderStruct(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
37+
const presets = this.getPresets('struct');
38+
const renderer = new StructRenderer(this.options, presets, model, inputModel);
39+
const result = await renderer.runSelfPreset();
40+
return RenderOutput.toRenderOutput({ result, dependencies: renderer.dependencies });
41+
}
42+
}

src/generators/go/GoPreset.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* eslint-disable @typescript-eslint/ban-types */
2+
import { AbstractRenderer } from '../AbstractRenderer';
3+
import { Preset, CommonModel, CommonPreset, PresetArgs } from '../../models';
4+
import { StructRenderer, GO_DEFAULT_STRUCT_PRESET } from './renderers/StructRenderer';
5+
6+
export interface FieldArgs {
7+
fieldName: string;
8+
field: CommonModel;
9+
}
10+
11+
export interface StructPreset<R extends AbstractRenderer, O extends object = any> extends CommonPreset<R, O> {
12+
ctor?: (args: PresetArgs<R, O>) => Promise<string> | string;
13+
field?: (args: PresetArgs<R, O> & FieldArgs) => Promise<string> | string;
14+
getter?: (args: PresetArgs<R, O> & FieldArgs) => Promise<string> | string;
15+
setter?: (args: PresetArgs<R, O> & FieldArgs) => Promise<string> | string;
16+
}
17+
18+
export type GoPreset = Preset<{
19+
struct: StructPreset<StructRenderer>;
20+
}>;
21+
22+
export const GO_DEFAULT_PRESET: GoPreset = {
23+
struct: GO_DEFAULT_STRUCT_PRESET,
24+
};

src/generators/go/GoRenderer.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { AbstractRenderer } from '../AbstractRenderer';
2+
import { GoOptions } from './GoGenerator';
3+
import { CommonModel, CommonInputModel, Preset } from '../../models';
4+
import { FormatHelpers } from '../../helpers/FormatHelpers';
5+
import { pascalCaseTransformMerge } from 'pascal-case';
6+
7+
/**
8+
* Common renderer for Go types
9+
*
10+
* @extends AbstractRenderer
11+
*/
12+
export abstract class GoRenderer extends AbstractRenderer<GoOptions> {
13+
constructor(
14+
options: GoOptions,
15+
presets: Array<[Preset, unknown]>,
16+
model: CommonModel,
17+
inputModel: CommonInputModel,
18+
) {
19+
super(options, presets, model, inputModel);
20+
}
21+
22+
async renderFields(): Promise<string> {
23+
const fields = this.model.properties || {};
24+
const content: string[] = [];
25+
26+
for (const [fieldName, field] of Object.entries(fields)) {
27+
const renderField = await this.runFieldPreset(fieldName, field);
28+
content.push(renderField);
29+
}
30+
31+
return this.renderBlock(content);
32+
}
33+
34+
runFieldPreset(fieldName: string, field: CommonModel): Promise<string> {
35+
return this.runPreset('field', { fieldName, field });
36+
}
37+
38+
renderType(model: CommonModel): string {
39+
if (model.$ref !== undefined) {
40+
return FormatHelpers.toPascalCase(model.$ref, { transform: pascalCaseTransformMerge });
41+
}
42+
43+
if (Array.isArray(model.type)) {
44+
return model.type.length > 1 ? '[]interface{}' : `[]${this.toGoType(model.type[0], model)}`;
45+
}
46+
47+
return this.toGoType(model.type, model);
48+
}
49+
50+
/* eslint-disable sonarjs/no-duplicate-string */
51+
toGoType(type: string | undefined, model: CommonModel): string {
52+
if (type === undefined) {
53+
return 'interface{}';
54+
}
55+
56+
switch (type) {
57+
case 'string':
58+
return 'string';
59+
case 'integer':
60+
return 'int';
61+
case 'number':
62+
return 'float64';
63+
case 'boolean':
64+
return 'bool';
65+
case 'object':
66+
return 'interface{}';
67+
case 'array': {
68+
if (Array.isArray(model.items)) {
69+
return model.items.length > 1? '[]interface{}' : `[]${this.renderType(model.items[0])}`;
70+
}
71+
const arrayType = model.items ? this.renderType(model.items) : 'interface{}';
72+
return `[]${arrayType}`;
73+
}
74+
default: return type;
75+
}
76+
}
77+
}

src/generators/go/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './GoGenerator';
2+
export { GO_DEFAULT_PRESET } from './GoPreset';
3+
export type { GoPreset } from './GoPreset';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { GoRenderer } from '../GoRenderer';
2+
import { StructPreset } from '../GoPreset';
3+
import { FormatHelpers } from '../../../helpers/FormatHelpers';
4+
import { pascalCaseTransformMerge } from 'pascal-case';
5+
6+
/**
7+
* Renderer for Go's `struct` type
8+
*
9+
* @extends GoRenderer
10+
*/
11+
export class StructRenderer extends GoRenderer {
12+
public async defaultSelf(): Promise<string> {
13+
const content = [
14+
await this.renderFields(),
15+
await this.runAdditionalContentPreset()
16+
];
17+
18+
const formattedName = this.model.$id && FormatHelpers.toPascalCase(this.model.$id, { transform: pascalCaseTransformMerge });
19+
const doc = this.renderComments(`${formattedName} represents a ${formattedName} model.`);
20+
21+
return `${doc}
22+
type ${formattedName} struct {
23+
${this.indent(this.renderBlock(content, 2))}
24+
}`;
25+
}
26+
27+
renderComments(lines: string | string[]): string {
28+
lines = FormatHelpers.breakLines(lines);
29+
return lines.map(line => `// ${line}`).join('\n');
30+
}
31+
}
32+
33+
export const GO_DEFAULT_STRUCT_PRESET: StructPreset<StructRenderer> = {
34+
self({ renderer }) {
35+
return renderer.defaultSelf();
36+
},
37+
ctor() {
38+
return 'thisShoulBeAConstructor';
39+
},
40+
field({ fieldName, field, renderer }) {
41+
return `${FormatHelpers.toPascalCase(fieldName, { transform: pascalCaseTransformMerge }) } ${ renderer.renderType(field)}`;
42+
},
43+
getter() {
44+
return 'getterFunc';
45+
},
46+
setter() {
47+
return 'setterFunc';
48+
},
49+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { GoGenerator } from '../../../src/generators/go/GoGenerator';
2+
3+
describe('GoGenerator', () => {
4+
let generator: GoGenerator;
5+
beforeEach(() => {
6+
generator = new GoGenerator();
7+
});
8+
test('should render `struct` type', async () => {
9+
const doc = {
10+
$id: '_address',
11+
type: 'object',
12+
properties: {
13+
street_name: { type: 'string' },
14+
city: { type: 'string', description: 'City description' },
15+
state: { type: 'string' },
16+
house_number: { type: 'number' },
17+
marriage: { type: 'boolean', description: 'Status if marriage live in given house' },
18+
members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], },
19+
tuple_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
20+
array_type: { type: 'array', items: { type: 'string' } },
21+
},
22+
required: ['street_name', 'city', 'state', 'house_number', 'array_type'],
23+
};
24+
const expected = `// Address represents a Address model.
25+
type Address struct {
26+
StreetName string
27+
City string
28+
State string
29+
HouseNumber float64
30+
Marriage bool
31+
Members []interface{}
32+
TupleType []interface{}
33+
ArrayType []string
34+
}`;
35+
36+
const inputModel = await generator.process(doc);
37+
const model = inputModel.models['_address'];
38+
39+
let structModel = await generator.renderStruct(model, inputModel);
40+
expect(structModel.result).toEqual(expected);
41+
expect(structModel.dependencies).toEqual([]);
42+
43+
structModel = await generator.render(model, inputModel);
44+
expect(structModel.result).toEqual(expected);
45+
expect(structModel.dependencies).toEqual([]);
46+
});
47+
48+
test('should work custom preset for `struct` type', async () => {
49+
const doc = {
50+
$id: 'CustomStruct',
51+
type: 'object',
52+
properties: {
53+
property: { type: 'string' },
54+
}
55+
};
56+
const expected = `// CustomStruct represents a CustomStruct model.
57+
type CustomStruct struct {
58+
property string
59+
}`;
60+
61+
generator = new GoGenerator({ presets: [
62+
{
63+
struct: {
64+
field({ fieldName, field, renderer }) {
65+
return `${fieldName} ${renderer.renderType(field)}`; // private fields
66+
},
67+
}
68+
}
69+
] });
70+
71+
const inputModel = await generator.process(doc);
72+
const model = inputModel.models['CustomStruct'];
73+
74+
const structModel = await generator.render(model, inputModel);
75+
expect(structModel.result).toEqual(expected);
76+
expect(structModel.dependencies).toEqual([]);
77+
});
78+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { GoRenderer } from '../../../src/generators/go/GoRenderer';
2+
import { CommonInputModel, CommonModel } from '../../../src/models';
3+
class MockGoRenderer extends GoRenderer {
4+
5+
}
6+
describe('GoRenderer', () => {
7+
let renderer: MockGoRenderer;
8+
beforeEach(() => {
9+
renderer = new MockGoRenderer({}, [], new CommonModel(), new CommonInputModel());
10+
});
11+
12+
describe('toGoType()', () => {
13+
test('Should render undefined as interface type', () => {
14+
expect(renderer.toGoType(undefined, new CommonModel())).toEqual('interface{}');
15+
});
16+
test('Should render integer as int type', () => {
17+
expect(renderer.toGoType('integer', new CommonModel())).toEqual('int');
18+
});
19+
test('Should render number as float64 type', () => {
20+
expect(renderer.toGoType('number', new CommonModel())).toEqual('float64');
21+
});
22+
test('Should render array as slice of the type', () => {
23+
const model = new CommonModel();
24+
model.items = CommonModel.toCommonModel({ type: 'number' });
25+
expect(renderer.toGoType('array', model)).toEqual('[]float64');
26+
});
27+
test('Should render tuple with one type as slice of that type', () => {
28+
const model = new CommonModel();
29+
model.items = [CommonModel.toCommonModel({ type: 'number' })];
30+
expect(renderer.toGoType('array', model)).toEqual('[]float64');
31+
});
32+
test('Should render tuple with multiple types as slice of interface{}', () => {
33+
const model = new CommonModel();
34+
model.items = [CommonModel.toCommonModel({ type: 'number' }), CommonModel.toCommonModel({ type: 'string' })];
35+
expect(renderer.toGoType('array', model)).toEqual('[]interface{}');
36+
});
37+
test('Should render object as interface type', () => {
38+
expect(renderer.toGoType('object', new CommonModel())).toEqual('interface{}');
39+
});
40+
});
41+
describe('renderType()', () => {
42+
test('Should render refs with pascal case (no _ prefix before numbers)', () => {
43+
const model = new CommonModel();
44+
model.$ref = '<anonymous-schema-1>';
45+
expect(renderer.renderType(model)).toEqual('AnonymousSchema1');
46+
});
47+
test('Should render union types with one type as slice of that type', () => {
48+
const model = CommonModel.toCommonModel({ type: ['number'] });
49+
expect(renderer.renderType(model)).toEqual('[]float64');
50+
});
51+
test('Should render union types with multiple types as slice of interface', () => {
52+
const model = CommonModel.toCommonModel({ type: ['number', 'string'] });
53+
expect(renderer.renderType(model)).toEqual('[]interface{}');
54+
});
55+
});
56+
});

0 commit comments

Comments
 (0)