Repository: asyncapi/modelina (ts)

Issue:
[BUG] go generator is unable to handle unions types correctly.
### Describe the bug.

Go generator cannot generate models for types that could be two separate types simultaneously, for example, a servers object that could be a list of server objects or a reference object. 

### Expected behavior

Go generator can generate models that support union types or replicate the functionality of union types in golang, with respect to what is possible in golang.

### Screenshots

![Screenshot 2024-02-28 at 2 39 22 PM](https://github.com/asyncapi/modelina/assets/41781438/23f648cf-e127-4f86-9302-9954b09c4989)

`servers` could be a reference type as well. 

### How to Reproduce

use `modelina` to generate models for the [AsyncAPI JSON scheme](https://github.com/asyncapi/spec-json-schemas/blob/master/schemas/3.0.0-without-%24id.json)

### 🥦 Browser

Mozilla Firefox

### 👀 Have you checked for similar open issues?

- [X] I checked and didn't find similar issue

### 🏢 Have you read the Contributing Guidelines?

- [X] I have read the [Contributing Guidelines](https://github.com/asyncapi/.github/blob/master/CONTRIBUTING.md)

### Are you willing to work on this issue ?

Yes I am willing to submit a PR!

Pull request:
feat: add json serialisation support for golang 
## Description
This PR is adding JSON serialization support by adding struct tags while generating go structs, by creating a new `GO_PRESET`.

```go
package models

type AsyncApi_3Dot_0Dot_0SchemaDot struct {
  Asyncapi string `json:"asyncapi"`
  Id string `json:"id"`
  Info *Info `json:"info"`
  Servers map[string]interface{} `json:"servers"`
  DefaultContentType string `json:"defaultContentType"`
  Channels map[string]interface{} `json:"channels"`
  Operations map[string]interface{} `json:"operations"`
  Components *Components `json:"components"`
  AdditionalProperties map[string]interface{} `json:"-"`
}
```

Also adds custom marshaler functions for enums when generating `struct-tags`

```go
func (op *ApiKeyIn) UnmarshalJSON(raw []byte) error {
	var v any
	if err := json.Unmarshal(raw, &v); err != nil {
		return err
	}
	*op = ValuesToApiKeyIn[v]
	return nil
}

func (op ApiKeyIn) MarshalJSON() ([]byte, error) {
	return json.Marshal(op.Value())
} 
          

```



## Related Issue
<!-- If this pull request is related to any existing issue, mention it here. -->
Fixes https://github.com/asyncapi/modelina/issues/299

## Checklist
- [x] The code follows the project's coding standards and is properly linted (`npm run lint`).
- [x] Tests have been added or updated to cover the changes.
- [x] Documentation has been updated to reflect the changes.
- [x] All tests pass successfully locally.(`npm run test`).

## Additional Notes
<!-- Add any additional information or context that might be relevant to reviewers. -->


Patch:
diff --git a/docs/languages/Go.md b/docs/languages/Go.md
index 7cfa209c..db08e246 100644
--- a/docs/languages/Go.md
+++ b/docs/languages/Go.md
@@ -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.
diff --git a/examples/go-json-tags/README.md b/examples/go-json-tags/README.md
new file mode 100644
index 00000000..dfbde05d
--- /dev/null
+++ b/examples/go-json-tags/README.md
@@ -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
+```
diff --git a/examples/go-json-tags/__snapshots__/index.spec.ts.snap b/examples/go-json-tags/__snapshots__/index.spec.ts.snap
new file mode 100644
index 00000000..d0a0aaf1
--- /dev/null
+++ b/examples/go-json-tags/__snapshots__/index.spec.ts.snap
@@ -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())
+}",
+]
+`;
diff --git a/examples/go-json-tags/index.spec.ts b/examples/go-json-tags/index.spec.ts
new file mode 100644
index 00000000..a9dd1db4
--- /dev/null
+++ b/examples/go-json-tags/index.spec.ts
@@ -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();
+  });
+});
diff --git a/examples/go-json-tags/index.ts b/examples/go-json-tags/index.ts
new file mode 100644
index 00000000..512318ef
--- /dev/null
+++ b/examples/go-json-tags/index.ts
@@ -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();
+}
diff --git a/examples/go-json-tags/package-lock.json b/examples/go-json-tags/package-lock.json
new file mode 100644
index 00000000..56ab4e87
--- /dev/null
+++ b/examples/go-json-tags/package-lock.json
@@ -0,0 +1,10 @@
+{
+  "name": "typescript-interface",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "hasInstallScript": true
+    }
+  }
+}
diff --git a/examples/go-json-tags/package.json b/examples/go-json-tags/package.json
new file mode 100644
index 00000000..30756a14
--- /dev/null
+++ b/examples/go-json-tags/package.json
@@ -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"
+  }
+}
diff --git a/src/generators/go/GoPreset.ts b/src/generators/go/GoPreset.ts
index d1d5ed29..7c7cce5c 100644
--- a/src/generators/go/GoPreset.ts
+++ b/src/generators/go/GoPreset.ts
@@ -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
 };
diff --git a/src/generators/go/index.ts b/src/generators/go/index.ts
index 62b65aa2..2a15ded4 100644
--- a/src/generators/go/index.ts
+++ b/src/generators/go/index.ts
@@ -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,
diff --git a/src/generators/go/presets/CommonPreset.ts b/src/generators/go/presets/CommonPreset.ts
new file mode 100644
index 00000000..8f1ae316
--- /dev/null
+++ b/src/generators/go/presets/CommonPreset.ts
@@ -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')}`;
+    }
+  }
+};
diff --git a/src/generators/go/presets/index.ts b/src/generators/go/presets/index.ts
new file mode 100644
index 00000000..58c93bfe
--- /dev/null
+++ b/src/generators/go/presets/index.ts
@@ -0,0 +1,1 @@
+export * from './CommonPreset';
diff --git a/src/generators/go/renderers/EnumRenderer.ts b/src/generators/go/renderers/EnumRenderer.ts
index 3607e122..05ac2241 100644
--- a/src/generators/go/renderers/EnumRenderer.ts
+++ b/src/generators/go/renderers/EnumRenderer.ts
@@ -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;
@@ -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> {


Test patch:
diff --git a/test/generators/go/__snapshots__/GoGenerator.spec.ts.snap b/test/generators/go/__snapshots__/GoGenerator.spec.ts.snap
index b400dc90..7b3aa589 100644
--- a/test/generators/go/__snapshots__/GoGenerator.spec.ts.snap
+++ b/test/generators/go/__snapshots__/GoGenerator.spec.ts.snap
@@ -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`] = `
diff --git a/test/generators/go/presets/CommonPreset.spec.ts b/test/generators/go/presets/CommonPreset.spec.ts
new file mode 100644
index 00000000..9647bdfe
--- /dev/null
+++ b/test/generators/go/presets/CommonPreset.spec.ts
@@ -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();
+  });
+});
diff --git a/test/generators/go/presets/__snapshots__/CommonPreset.spec.ts.snap b/test/generators/go/presets/__snapshots__/CommonPreset.spec.ts.snap
new file mode 100644
index 00000000..2b8d2f83
--- /dev/null
+++ b/test/generators/go/presets/__snapshots__/CommonPreset.spec.ts.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GO_COMMON_PRESET should render json tags for structs 1`] = `
+"// Root represents a Root model.
+type Root struct {
+  StringProp string \`json:\\"stringProp\\"\`
+  NumberProp float64 \`json:\\"numberProp\\"\`
+  BooleanProp bool \`json:\\"booleanProp\\"\`
+}"
+`;
+
+exports[`GO_COMMON_PRESET should render the marshal functions for enum 1`] = `
+"// Root represents an enum of Root.
+type Root uint
+
+const (
+  RootNumber_2Dot_6Dot_0 Root = iota
+)
+
+// Value returns the value of the enum.
+func (op Root) Value() any {
+	if op >= Root(len(RootValues)) {
+		return nil
+	}
+	return RootValues[op]
+}
+
+var RootValues = []any{\\"2.6.0\\"}
+var ValuesToRoot = map[any]Root{
+  RootValues[RootNumber_2Dot_6Dot_0]: RootNumber_2Dot_6Dot_0,
+}
+
+ 
+func (op *Root) UnmarshalJSON(raw []byte) error {
+  var v any
+  if err := json.Unmarshal(raw, &v); err != nil {
+  return err
+  }
+  *op = ValuesToRoot[v]
+  return nil
+}
+
+func (op Root) MarshalJSON() ([]byte, error) {
+  return json.Marshal(op.Value())
+}"
+`;