Skip to content

Commit

Permalink
feat: add json serialisation support for golang (#1848)
Browse files Browse the repository at this point in the history
  • Loading branch information
Souvikns authored Mar 7, 2024
1 parent dca4c98 commit 0b0f56e
Show file tree
Hide file tree
Showing 15 changed files with 358 additions and 7 deletions.
6 changes: 6 additions & 0 deletions docs/languages/Go.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ There are special use-cases that each language supports; this document pertains

<!-- tocstop -->

## Generate JSON struct-tags and marshaler functions for enums

To generate go models that works correctly with JSON marshal functions we need to generate appropriate JSON `struct-tags`, use the preset `GO_COMMON_PRESET` and provide the option `addJsonTag: true`.

check out this [example for a live demonstration](../../examples/go-json-tags/)

## Generate serializer and deserializer functionality

The most widely used usecase for Modelina is to generate models that include serialization and deserialization functionality to convert the models into payload data. This payload data can of course be many different kinds, JSON, XML, raw binary, you name it.
Expand Down
17 changes: 17 additions & 0 deletions examples/go-json-tags/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Go Struct Tags In Models

`GO_COMMON_PRESET` to render JSON `struct-tags` in go `structs` along with adding custom `Marshaler` functions for enum.

## How to run this example

Run this example using:

```sh
npm i && npm run start
```

If you are on Windows, use the `start:windows` script instead:

```sh
npm i && npm run start:windows
```
97 changes: 97 additions & 0 deletions examples/go-json-tags/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Should be able to render json-tags in struct and should log expected output to console 1`] = `
Array [
"// Root represents a Root model.
type Root struct {
Cities *Cities \`json:\\"cities\\"\`
Options *Options \`json:\\"options\\"\`
}",
]
`;
exports[`Should be able to render json-tags in struct and should log expected output to console 2`] = `
Array [
"// Cities represents an enum of Cities.
type Cities uint
const (
CitiesLondon Cities = iota
CitiesRome
CitiesBrussels
)
// Value returns the value of the enum.
func (op Cities) Value() any {
if op >= Cities(len(CitiesValues)) {
return nil
}
return CitiesValues[op]
}
var CitiesValues = []any{\\"London\\",\\"Rome\\",\\"Brussels\\"}
var ValuesToCities = map[any]Cities{
CitiesValues[CitiesLondon]: CitiesLondon,
CitiesValues[CitiesRome]: CitiesRome,
CitiesValues[CitiesBrussels]: CitiesBrussels,
}
func (op *Cities) UnmarshalJSON(raw []byte) error {
var v any
if err := json.Unmarshal(raw, &v); err != nil {
return err
}
*op = ValuesToCities[v]
return nil
}
func (op Cities) MarshalJSON() ([]byte, error) {
return json.Marshal(op.Value())
}",
]
`;
exports[`Should be able to render json-tags in struct and should log expected output to console 3`] = `
Array [
"// Options represents an enum of Options.
type Options uint
const (
OptionsNumber_123 Options = iota
OptionsNumber_213
OptionsTrue
OptionsRun
)
// Value returns the value of the enum.
func (op Options) Value() any {
if op >= Options(len(OptionsValues)) {
return nil
}
return OptionsValues[op]
}
var OptionsValues = []any{123,213,true,\\"Run\\"}
var ValuesToOptions = map[any]Options{
OptionsValues[OptionsNumber_123]: OptionsNumber_123,
OptionsValues[OptionsNumber_213]: OptionsNumber_213,
OptionsValues[OptionsTrue]: OptionsTrue,
OptionsValues[OptionsRun]: OptionsRun,
}
func (op *Options) UnmarshalJSON(raw []byte) error {
var v any
if err := json.Unmarshal(raw, &v); err != nil {
return err
}
*op = ValuesToOptions[v]
return nil
}
func (op Options) MarshalJSON() ([]byte, error) {
return json.Marshal(op.Value())
}",
]
`;
17 changes: 17 additions & 0 deletions examples/go-json-tags/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const spy = jest.spyOn(global.console, 'log').mockImplementation(() => {
return;
});
import { generate } from './index';

describe('Should be able to render json-tags in struct', () => {
afterAll(() => {
jest.restoreAllMocks();
});
test('and should log expected output to console', async () => {
await generate();
expect(spy.mock.calls.length).toEqual(3);
expect(spy.mock.calls[0]).toMatchSnapshot();
expect(spy.mock.calls[1]).toMatchSnapshot();
expect(spy.mock.calls[2]).toMatchSnapshot();
});
});
37 changes: 37 additions & 0 deletions examples/go-json-tags/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
GoGenerator,
GO_COMMON_PRESET,
GoCommonPresetOptions
} from '../../src';

const options: GoCommonPresetOptions = { addJsonTag: true };
const generator = new GoGenerator({
presets: [{ preset: GO_COMMON_PRESET, options }]
});
const jsonSchemaDraft7 = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
additionalProperties: false,
properties: {
cities: {
$id: 'cities',
type: 'string',
enum: ['London', 'Rome', 'Brussels']
},
options: {
$id: 'options',
type: ['integer', 'boolean', 'string'],
enum: [123, 213, true, 'Run']
}
}
};

export async function generate(): Promise<void> {
const models = await generator.generate(jsonSchemaDraft7);
for (const model of models) {
console.log(model.result);
}
}
if (require.main === module) {
generate();
}
10 changes: 10 additions & 0 deletions examples/go-json-tags/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions examples/go-json-tags/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"config" : { "example_name" : "go-json-tags" },
"scripts": {
"install": "cd ../.. && npm i",
"start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts",
"start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts",
"test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts",
"test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts"
}
}
4 changes: 2 additions & 2 deletions src/generators/go/GoPreset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ interface EnumPreset<R extends AbstractRenderer, O>
export type StructPresetType<O> = StructPreset<StructRenderer, O>;
export type EnumPresetType<O> = EnumPreset<EnumRenderer, O>;

export type GoPreset<O = GoOptions> = Preset<{
export type GoPreset<O = any> = Preset<{
struct: StructPresetType<O>;
enum: EnumPresetType<O>;
}>;

export const GO_DEFAULT_PRESET: GoPreset = {
export const GO_DEFAULT_PRESET: GoPreset<GoOptions> = {
struct: GO_DEFAULT_STRUCT_PRESET,
enum: GO_DEFAULT_ENUM_PRESET
};
1 change: 1 addition & 0 deletions src/generators/go/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './GoGenerator';
export * from './GoFileGenerator';
export { GO_DEFAULT_PRESET } from './GoPreset';
export type { GoPreset } from './GoPreset';
export * from './presets';

export {
defaultEnumKeyConstraints as goDefaultEnumKeyConstraints,
Expand Down
70 changes: 70 additions & 0 deletions src/generators/go/presets/CommonPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { GoPreset } from '../GoPreset';
import { GoRenderer } from '../GoRenderer';
import {
ConstrainedDictionaryModel,
ConstrainedObjectPropertyModel,
ConstrainedEnumModel
} from '../../../models';

export interface GoCommonPresetOptions {
addJsonTag: boolean;
}

function renderJSONTag({
field
}: {
field: ConstrainedObjectPropertyModel;
}): string {
if (
field.property instanceof ConstrainedDictionaryModel &&
field.property.serializationType === 'unwrap'
) {
return `json:"-"`;
}
return `json:"${field.unconstrainedPropertyName}"`;
}

function renderMarshallingFunctions({
model,
renderer
}: {
model: ConstrainedEnumModel;
renderer: GoRenderer<any>;
}): string {
renderer.dependencyManager.addDependency('encoding/json');
return `
func (op *${model.name}) UnmarshalJSON(raw []byte) error {
var v any
if err := json.Unmarshal(raw, &v); err != nil {
return err
}
*op = ValuesTo${model.name}[v]
return nil
}
func (op ${model.name}) MarshalJSON() ([]byte, error) {
return json.Marshal(op.Value())
}`;
}

export const GO_COMMON_PRESET: GoPreset<GoCommonPresetOptions> = {
struct: {
field: ({ content, field, options }) => {
const blocks: string[] = [];
if (options.addJsonTag) {
blocks.push(renderJSONTag({ field }));
}
return `${content} \`${blocks.join(' ')}\``;
}
},
enum: {
additionalContent({ content, model, renderer, options }) {
const blocks: string[] = [];
if (options.addJsonTag) {
blocks.push(renderMarshallingFunctions({ model, renderer }));
}

return `${content}\n ${blocks.join('\n')}`;
}
}
};
1 change: 1 addition & 0 deletions src/generators/go/presets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CommonPreset';
6 changes: 2 additions & 4 deletions src/generators/go/renderers/EnumRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ export class EnumRenderer extends GoRenderer<ConstrainedEnumModel> {
return `${this.model.name}Values[${value.key}]: ${value.key},`;
});
const additionalContent = await this.runAdditionalContentPreset();
const renderedAdditionalContent = additionalContent
? this.indent(additionalContent)
: '';

const values = this.model.values
.map((value) => {
return value.value;
Expand All @@ -50,7 +48,7 @@ var ${this.model.name}Values = []any{${values}}
var ValuesTo${this.model.name} = map[any]${this.model.name}{
${this.indent(this.renderBlock(valuesToEnumMap))}
}
${renderedAdditionalContent}`;
${additionalContent}`;
}

async renderItems(): Promise<string> {
Expand Down
2 changes: 1 addition & 1 deletion test/generators/go/__snapshots__/GoGenerator.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ var ValuesToCustomEnum = map[any]CustomEnum{
CustomEnumValues[CustomEnumAlabama]: CustomEnumAlabama,
CustomEnumValues[CustomEnumCalifornia]: CustomEnumCalifornia,
}
additionalContent"
additionalContent"
`;

exports[`GoGenerator should work custom preset for \`struct\` type 1`] = `
Expand Down
41 changes: 41 additions & 0 deletions test/generators/go/presets/CommonPreset.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
GoGenerator,
GO_COMMON_PRESET,
GoCommonPresetOptions
} from '../../../../src/generators';

describe('GO_COMMON_PRESET', () => {
let generator: GoGenerator;
beforeEach(() => {
const options: GoCommonPresetOptions = { addJsonTag: true };
generator = new GoGenerator({
presets: [{ preset: GO_COMMON_PRESET, options }]
});
});

test('should render json tags for structs', async () => {
const doc = {
type: 'object',
properties: {
stringProp: { type: 'string' },
numberProp: { type: 'number' },
booleanProp: { type: 'boolean' }
},
additionalProperties: false
};

const models = await generator.generate(doc);
expect(models).toHaveLength(1);
expect(models[0].result).toMatchSnapshot();
});

test('should render the marshal functions for enum', async () => {
const doc = {
type: 'string',
enum: ['2.6.0']
};
const models = await generator.generate(doc);
expect(models).toHaveLength(1);
expect(models[0].result).toMatchSnapshot();
});
});
Loading

0 comments on commit 0b0f56e

Please sign in to comment.