From ecd9618ced4c8ee0fa5328ec5af3471f7a6f79ac Mon Sep 17 00:00:00 2001 From: Kenneth Aasan Date: Thu, 25 Apr 2024 20:32:09 +0200 Subject: [PATCH] fix: fixes union type with discriminator in go (#1967) --- docs/migrations/version-3-to-4.md | 12 +- src/generators/go/renderers/StructRenderer.ts | 39 ++-- src/generators/go/renderers/UnionRenderer.ts | 24 ++- test/generators/go/GoGenerator.spec.ts | 196 +++++++++++++----- .../go/__snapshots__/GoGenerator.spec.ts.snap | 177 ++++++++++------ 5 files changed, 307 insertions(+), 141 deletions(-) diff --git a/docs/migrations/version-3-to-4.md b/docs/migrations/version-3-to-4.md index 2aa33952b7..525b445ba1 100644 --- a/docs/migrations/version-3-to-4.md +++ b/docs/migrations/version-3-to-4.md @@ -261,7 +261,7 @@ While the above changes work for primitives, it's problematic for objects with a ```go type Vehicle interface { - IsVehicleType() bool + IsVehicleVehicleType() } type Car struct { @@ -270,9 +270,7 @@ type Car struct { AdditionalProperties map[string]interface{} } -func (serdp Car) IsVehicleType() bool { - return true -} +func (r Car) IsVehicleVehicleType() {} type Truck struct { VehicleType *VehicleType @@ -280,9 +278,7 @@ type Truck struct { AdditionalProperties map[string]interface{} } -func (serdp Truck) IsVehicleType() bool { - return true -} +func (r Truck) IsVehicleVehicleType() {} type VehicleType uint @@ -313,7 +309,7 @@ Modelina now has support for nullable and required properties in go structs. Thi ```go type info struct { name string // required - description *string // nullable + description *string // nullable version *float64 isDevelopment *bool } diff --git a/src/generators/go/renderers/StructRenderer.ts b/src/generators/go/renderers/StructRenderer.ts index e2bfa9e243..b75bf8716d 100644 --- a/src/generators/go/renderers/StructRenderer.ts +++ b/src/generators/go/renderers/StructRenderer.ts @@ -3,7 +3,8 @@ import { StructPresetType } from '../GoPreset'; import { ConstrainedObjectModel, ConstrainedObjectPropertyModel, - ConstrainedReferenceModel + ConstrainedReferenceModel, + ConstrainedUnionModel } from '../../../models'; import { GoOptions } from '../GoGenerator'; import { FormatHelpers } from '../../../helpers/FormatHelpers'; @@ -26,10 +27,7 @@ export class StructRenderer extends GoRenderer { let discriminator = ''; - if ( - this.model.options.parents?.length && - this.model.options.discriminator?.discriminator - ) { + if (this.model.options.parents?.length) { discriminator = await this.runDiscriminatorFuncPreset(); } @@ -71,20 +69,37 @@ export const GO_DEFAULT_STRUCT_PRESET: StructPresetType = { }, field({ field }) { let fieldType = field.property.type; - if (field.property instanceof ConstrainedReferenceModel) { + if ( + field.property instanceof ConstrainedReferenceModel && + !( + field.property.ref instanceof ConstrainedUnionModel && + field.property.ref.options.discriminator + ) + ) { fieldType = `*${fieldType}`; } return `${field.propertyName} ${fieldType}`; }, discriminator({ model }) { - if (!model.options.discriminator?.discriminator) { + const { parents } = model.options; + + if (!parents?.length) { return ''; } - return `func (serdp ${model.name}) Is${FormatHelpers.toPascalCase( - model.options.discriminator.discriminator - )}() bool { - return true -}`; + return parents + .map((parent) => { + if (!parent.options.discriminator) { + return undefined; + } + + return `func (r ${model.name}) Is${FormatHelpers.toPascalCase( + parent.name + )}${FormatHelpers.toPascalCase( + parent.options.discriminator.discriminator + )}() {}`; + }) + .filter((parent) => !!parent) + .join('\n\n'); } }; diff --git a/src/generators/go/renderers/UnionRenderer.ts b/src/generators/go/renderers/UnionRenderer.ts index 0caa90f967..1c941fc25a 100644 --- a/src/generators/go/renderers/UnionRenderer.ts +++ b/src/generators/go/renderers/UnionRenderer.ts @@ -9,12 +9,15 @@ import { import { GoOptions } from '../GoGenerator'; import { FormatHelpers } from '../../../helpers/FormatHelpers'; -const unionIncludesPrimitives = (model: ConstrainedUnionModel): boolean => { - return !model.union.every( - (union) => - union instanceof ConstrainedObjectModel || - (union instanceof ConstrainedReferenceModel && - union.ref instanceof ConstrainedObjectModel) +const unionIncludesDiscriminator = (model: ConstrainedUnionModel): boolean => { + return ( + !!model.options.discriminator && + model.union.every( + (union) => + union instanceof ConstrainedObjectModel || + (union instanceof ConstrainedReferenceModel && + union.ref instanceof ConstrainedObjectModel) + ) ); }; @@ -29,10 +32,7 @@ export class UnionRenderer extends GoRenderer { `${this.model.name} represents a ${this.model.name} model.` ); - if ( - !unionIncludesPrimitives(this.model) && - this.model.options.discriminator - ) { + if (unionIncludesDiscriminator(this.model)) { const content: string[] = [await this.runDiscriminatorAccessorPreset()]; return `${doc} @@ -98,7 +98,9 @@ export const GO_DEFAULT_UNION_PRESET: UnionPresetType = { } return `Is${FormatHelpers.toPascalCase( + model.name + )}${FormatHelpers.toPascalCase( model.options.discriminator.discriminator - )}() bool`; + )}()`; } }; diff --git a/test/generators/go/GoGenerator.spec.ts b/test/generators/go/GoGenerator.spec.ts index 566e5aaf62..60dff28b92 100644 --- a/test/generators/go/GoGenerator.spec.ts +++ b/test/generators/go/GoGenerator.spec.ts @@ -95,75 +95,167 @@ describe('GoGenerator', () => { expect(result).toMatchSnapshot(); }); - test('should render interfaces for objects with discriminator', async () => { - const asyncapiDoc = { - asyncapi: '2.6.0', - info: { - title: 'Vehicle example', - version: '1.0.0' - }, - channels: {}, - components: { - messages: { - Cargo: { - payload: { - title: 'Cargo', + describe('oneOf/discriminator', () => { + test('should render interfaces for objects with discriminator', async () => { + const asyncapiDoc = { + asyncapi: '2.6.0', + info: { + title: 'Vehicle example', + version: '1.0.0' + }, + channels: {}, + components: { + messages: { + Cargo: { + payload: { + title: 'Cargo', + type: 'object', + properties: { + vehicle: { + $ref: '#/components/schemas/Vehicle' + } + } + } + } + }, + schemas: { + Vehicle: { + title: 'Vehicle', + type: 'object', + discriminator: 'vehicleType', + properties: { + vehicleType: { + title: 'VehicleType', + type: 'string' + }, + registrationPlate: { + title: 'RegistrationPlate', + type: 'string' + } + }, + required: ['vehicleType', 'registrationPlate'], + oneOf: [ + { + $ref: '#/components/schemas/Car' + }, + { + $ref: '#/components/schemas/Truck' + } + ] + }, + Car: { + type: 'object', + properties: { + vehicleType: { + const: 'Car' + } + } + }, + Truck: { type: 'object', properties: { - vehicle: { - $ref: '#/components/schemas/Vehicle' + vehicleType: { + const: 'Truck' } } } } + } + }; + + const models = await generator.generate(asyncapiDoc); + expect(models.map((model) => model.result)).toMatchSnapshot(); + }); + + test('handle setting title with const', async () => { + const asyncapiDoc = { + asyncapi: '2.5.0', + info: { + title: 'CloudEvent example', + version: '1.0.0' }, - schemas: { - Vehicle: { - title: 'Vehicle', - type: 'object', - discriminator: 'vehicleType', - properties: { - vehicleType: { - title: 'VehicleType', - type: 'string' - }, - registrationPlate: { - title: 'RegistrationPlate', - type: 'string' + channels: { + pet: { + publish: { + message: { + oneOf: [ + { + $ref: '#/components/messages/Dog' + }, + { + $ref: '#/components/messages/Cat' + } + ] } - }, - required: ['vehicleType', 'registrationPlate'], - oneOf: [ - { - $ref: '#/components/schemas/Car' - }, - { - $ref: '#/components/schemas/Truck' + } + } + }, + components: { + messages: { + Dog: { + payload: { + title: 'Dog', + allOf: [ + { + $ref: '#/components/schemas/CloudEvent' + }, + { + $ref: '#/components/schemas/Dog' + } + ] } - ] - }, - Car: { - type: 'object', - properties: { - vehicleType: { - const: 'Car' + }, + Cat: { + payload: { + title: 'Cat', + allOf: [ + { + $ref: '#/components/schemas/CloudEvent' + }, + { + $ref: '#/components/schemas/Cat' + } + ] } } }, - Truck: { - type: 'object', - properties: { - vehicleType: { - const: 'Truck' + schemas: { + CloudEvent: { + title: 'CloudEvent', + type: 'object', + discriminator: 'type', + properties: { + type: { + type: 'string' + } + }, + required: ['type'] + }, + Dog: { + type: 'object', + properties: { + type: { + title: 'DogType', + const: 'Dog' + } + } + }, + Cat: { + type: 'object', + properties: { + type: { + title: 'CatType', + const: 'Cat' + } } } } } - } - }; + }; - const models = await generator.generate(asyncapiDoc); - expect(models.map((model) => model.result)).toMatchSnapshot(); + const models = await generator.generate(asyncapiDoc); + expect(models.map((model) => model.result)).toMatchSnapshot(); + }); }); test('should work custom preset for `struct` type', async () => { diff --git a/test/generators/go/__snapshots__/GoGenerator.spec.ts.snap b/test/generators/go/__snapshots__/GoGenerator.spec.ts.snap index 76e38abc92..1ce9dade56 100644 --- a/test/generators/go/__snapshots__/GoGenerator.spec.ts.snap +++ b/test/generators/go/__snapshots__/GoGenerator.spec.ts.snap @@ -132,6 +132,125 @@ type OtherModel struct { }" `; +exports[`GoGenerator oneOf/discriminator handle setting title with const 1`] = ` +Array [ + "// Pet represents a Pet model. +type Pet interface { + IsPetType() +}", + "// Dog represents a Dog model. +type Dog struct { + ReservedType *DogType + AdditionalProperties map[string]interface{} +} + +func (r Dog) IsPetType() {} +", + "// DogType represents an enum of DogType. +type DogType uint + +const ( + DogTypeDog DogType = iota +) + +// Value returns the value of the enum. +func (op DogType) Value() any { + if op >= DogType(len(DogTypeValues)) { + return nil + } + return DogTypeValues[op] +} + +var DogTypeValues = []any{\\"Dog\\"} +var ValuesToDogType = map[any]DogType{ + DogTypeValues[DogTypeDog]: DogTypeDog, +} +", + "// Cat represents a Cat model. +type Cat struct { + ReservedType *CatType + AdditionalProperties map[string]interface{} +} + +func (r Cat) IsPetType() {} +", + "// CatType represents an enum of CatType. +type CatType uint + +const ( + CatTypeCat CatType = iota +) + +// Value returns the value of the enum. +func (op CatType) Value() any { + if op >= CatType(len(CatTypeValues)) { + return nil + } + return CatTypeValues[op] +} + +var CatTypeValues = []any{\\"Cat\\"} +var ValuesToCatType = map[any]CatType{ + CatTypeValues[CatTypeCat]: CatTypeCat, +} +", +] +`; + +exports[`GoGenerator oneOf/discriminator should render interfaces for objects with discriminator 1`] = ` +Array [ + "// Vehicle represents a Vehicle model. +type Vehicle interface { + IsVehicleVehicleType() +}", + "// Cargo represents a Cargo model. +type Cargo struct { + Vehicle Vehicle + AdditionalProperties map[string]interface{} +}", + "// Car represents a Car model. +type Car struct { + VehicleType *VehicleType + RegistrationPlate string + AdditionalProperties map[string]interface{} +} + +func (r Car) IsVehicleVehicleType() {} +", + "// VehicleType represents an enum of VehicleType. +type VehicleType uint + +const ( + VehicleTypeCar VehicleType = iota + VehicleTypeTruck +) + +// Value returns the value of the enum. +func (op VehicleType) Value() any { + if op >= VehicleType(len(VehicleTypeValues)) { + return nil + } + return VehicleTypeValues[op] +} + +var VehicleTypeValues = []any{\\"Car\\",\\"Truck\\"} +var ValuesToVehicleType = map[any]VehicleType{ + VehicleTypeValues[VehicleTypeCar]: VehicleTypeCar, + VehicleTypeValues[VehicleTypeTruck]: VehicleTypeTruck, +} +", + "// Truck represents a Truck model. +type Truck struct { + VehicleType *VehicleType + RegistrationPlate string + AdditionalProperties map[string]interface{} +} + +func (r Truck) IsVehicleVehicleType() {} +", +] +`; + exports[`GoGenerator should render \`enum\` with mixed types 1`] = ` "// Things represents an enum of Things. type Things uint @@ -250,64 +369,6 @@ type LocationAdditionalPropertyOneOf_1 struct { }" `; -exports[`GoGenerator should render interfaces for objects with discriminator 1`] = ` -Array [ - "// Vehicle represents a Vehicle model. -type Vehicle interface { - IsVehicleType() bool -}", - "// Cargo represents a Cargo model. -type Cargo struct { - Vehicle *Vehicle - AdditionalProperties map[string]interface{} -}", - "// Car represents a Car model. -type Car struct { - VehicleType *VehicleType - RegistrationPlate string - AdditionalProperties map[string]interface{} -} - -func (serdp Car) IsVehicleType() bool { - return true -} -", - "// VehicleType represents an enum of VehicleType. -type VehicleType uint - -const ( - VehicleTypeCar VehicleType = iota - VehicleTypeTruck -) - -// Value returns the value of the enum. -func (op VehicleType) Value() any { - if op >= VehicleType(len(VehicleTypeValues)) { - return nil - } - return VehicleTypeValues[op] -} - -var VehicleTypeValues = []any{\\"Car\\",\\"Truck\\"} -var ValuesToVehicleType = map[any]VehicleType{ - VehicleTypeValues[VehicleTypeCar]: VehicleTypeCar, - VehicleTypeValues[VehicleTypeTruck]: VehicleTypeTruck, -} -", - "// Truck represents a Truck model. -type Truck struct { - VehicleType *VehicleType - RegistrationPlate string - AdditionalProperties map[string]interface{} -} - -func (serdp Truck) IsVehicleType() bool { - return true -} -", -] -`; - exports[`GoGenerator should work custom preset for \`enum\` type 1`] = ` "// CustomEnum represents an enum of CustomEnum. type CustomEnum uint