From 446fcdfdcd3a86e337adac88f8cdcb24e57406ea Mon Sep 17 00:00:00 2001 From: Gildas Lebel Date: Tue, 24 Sep 2024 11:37:38 -0400 Subject: [PATCH] feat(codegen): Add a flag to force field generation as pointers to make validations work --- README.md | 20 ++ cmd/asyncapi-codegen/config.go | 5 + examples/ping/v2/kafka/app/app.gen.go | 4 +- examples/ping/v2/kafka/user/user.gen.go | 4 +- .../ping/v2/nats-jetstream/app/app.gen.go | 4 +- .../ping/v2/nats-jetstream/user/user.gen.go | 4 +- examples/ping/v2/nats/app/app.gen.go | 4 +- examples/ping/v2/nats/user/user.gen.go | 4 +- pkg/codegen/codegen.go | 6 + pkg/codegen/generators/helpers.go | 4 +- .../generators/v2/templates/helpers.go | 12 + .../v2/templates/schema_definition.tmpl | 2 +- .../generators/v3/templates/helpers.go | 12 + .../v3/templates/schema_definition.tmpl | 2 +- pkg/codegen/options/options.go | 3 + test/v2/issues/131/asyncapi.gen.go | 2 +- test/v2/issues/131/suite_test.go | 7 - test/v2/issues/185/asyncapi.gen.go | 12 +- test/v2/issues/245/asyncapi.gen.go | 2 +- test/v2/issues/259/asyncapi.yaml | 27 ++ test/v2/issues/259/default/asyncapi.gen.go | 320 ++++++++++++++++++ .../issues/259/forcepointers/asyncapi.gen.go | 320 ++++++++++++++++++ test/v2/issues/259/suite_test.go | 106 ++++++ test/v2/issues/73/suite_test.go | 4 +- test/v2/issues/73/v2/asyncapi.gen.go | 4 +- test/v2/issues/74/asyncapi.gen.go | 10 +- test/v3/issues/131/asyncapi.gen.go | 2 +- test/v3/issues/131/suite_test.go | 7 - test/v3/issues/181/asyncapi.gen.go | 2 +- test/v3/issues/185/asyncapi.gen.go | 12 +- test/v3/issues/241/asyncapi.gen.go | 2 +- test/v3/issues/245/asyncapi.gen.go | 2 +- test/v3/issues/259/asyncapi.yaml | 27 ++ test/v3/issues/259/default/asyncapi.gen.go | 320 ++++++++++++++++++ .../issues/259/forcepointers/asyncapi.gen.go | 320 ++++++++++++++++++ test/v3/issues/259/suite_test.go | 107 ++++++ 36 files changed, 1648 insertions(+), 57 deletions(-) create mode 100644 test/v2/issues/259/asyncapi.yaml create mode 100644 test/v2/issues/259/default/asyncapi.gen.go create mode 100644 test/v2/issues/259/forcepointers/asyncapi.gen.go create mode 100644 test/v2/issues/259/suite_test.go create mode 100644 test/v3/issues/259/asyncapi.yaml create mode 100644 test/v3/issues/259/default/asyncapi.gen.go create mode 100644 test/v3/issues/259/forcepointers/asyncapi.gen.go create mode 100644 test/v3/issues/259/suite_test.go diff --git a/README.md b/README.md index 4112f2a..f45363a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ to the application/user. Just plug your application to your favorite message bro * [Versioning](#versioning) * [Extensions](#specification-extensions) * [ErrorHandler](#errorhandler) + * [Validations](#validations) * [Contributing and support](#contributing-and-support) ## Supported functionalities @@ -779,6 +780,25 @@ func(ctx context.Context, topic string, msg *extensions.AcknowledgeableBrokerMes } ``` +### Validations + +You can use [go-playground/validator](https://github.com/go-playground/validator) to validate the fields content against the contract. + +The following tags are currently supported: + +| Asyncapi | Validator tag | Comment | +|------------------|----------------|--------------------------------------------------------------| +| required | required | For a full support, the flag `--force-pointers` is necessary | +| minLength | min | | +| maxLength | max | | +| minimum | gte | | +| maximum | lte | | +| exclusiveMinimum | gt | | +| exclusiveMaximum | lt | | +| uniqueItems | unique | Only for arrays | +| enum | oneof | Only string enum are supported | + + ## Contributing and support If you find any bug or lacking a feature, please raise an issue on the Github repository! diff --git a/cmd/asyncapi-codegen/config.go b/cmd/asyncapi-codegen/config.go index 9d1c8a0..1d09fd8 100644 --- a/cmd/asyncapi-codegen/config.go +++ b/cmd/asyncapi-codegen/config.go @@ -46,6 +46,9 @@ type Flags struct { // IgnoreStringFormat states whether the properties' format (date, date-time) should impact the type in types IgnoreStringFormat bool + + // ForcePointers can be used to force all struct fields to be generated as pointers + ForcePointers bool } // SetToCommand adds the flags to a cobra command. @@ -63,6 +66,7 @@ func (f *Flags) SetToCommand(cmd *cobra.Command) { "Naming scheme for generated golang elements.\nSupported values: camel, none.") cmd.Flags().BoolVar(&f.IgnoreStringFormat, "ignore-string-format", false, "Ignores the format (date, date-time) on string properties, generating golang string, instead of dates") + cmd.Flags().BoolVar(&f.ForcePointers, "force-pointers", false, "Forces all struct fields to be generated as pointers") } // ToCodegenOptions processes command line flags structure to code generation tool options. @@ -74,6 +78,7 @@ func (f Flags) ToCodegenOptions() (options.Options, error) { ConvertKeys: f.ConvertKeys, NamingScheme: f.NamingScheme, IgnoreStringFormat: f.IgnoreStringFormat, + ForcePointers: f.ForcePointers, } if f.Generate != "" { diff --git a/examples/ping/v2/kafka/app/app.gen.go b/examples/ping/v2/kafka/app/app.gen.go index 345f49c..0f81a15 100644 --- a/examples/ping/v2/kafka/app/app.gen.go +++ b/examples/ping/v2/kafka/app/app.gen.go @@ -462,10 +462,10 @@ type PongMessageHeaders struct { // PongMessagePayload is a schema from the AsyncAPI specification required in messages type PongMessagePayload struct { // Description: Pong message - Message string `json:"message" validate:"required"` + Message string `json:"message"` // Description: Pong creation time - Time time.Time `json:"time" validate:"required"` + Time time.Time `json:"time"` } // PongMessage is the message expected for 'PongMessage' channel. diff --git a/examples/ping/v2/kafka/user/user.gen.go b/examples/ping/v2/kafka/user/user.gen.go index 9a010aa..9fd25ba 100644 --- a/examples/ping/v2/kafka/user/user.gen.go +++ b/examples/ping/v2/kafka/user/user.gen.go @@ -577,10 +577,10 @@ type PongMessageHeaders struct { // PongMessagePayload is a schema from the AsyncAPI specification required in messages type PongMessagePayload struct { // Description: Pong message - Message string `json:"message" validate:"required"` + Message string `json:"message"` // Description: Pong creation time - Time time.Time `json:"time" validate:"required"` + Time time.Time `json:"time"` } // PongMessage is the message expected for 'PongMessage' channel. diff --git a/examples/ping/v2/nats-jetstream/app/app.gen.go b/examples/ping/v2/nats-jetstream/app/app.gen.go index 345f49c..0f81a15 100644 --- a/examples/ping/v2/nats-jetstream/app/app.gen.go +++ b/examples/ping/v2/nats-jetstream/app/app.gen.go @@ -462,10 +462,10 @@ type PongMessageHeaders struct { // PongMessagePayload is a schema from the AsyncAPI specification required in messages type PongMessagePayload struct { // Description: Pong message - Message string `json:"message" validate:"required"` + Message string `json:"message"` // Description: Pong creation time - Time time.Time `json:"time" validate:"required"` + Time time.Time `json:"time"` } // PongMessage is the message expected for 'PongMessage' channel. diff --git a/examples/ping/v2/nats-jetstream/user/user.gen.go b/examples/ping/v2/nats-jetstream/user/user.gen.go index 9a010aa..9fd25ba 100644 --- a/examples/ping/v2/nats-jetstream/user/user.gen.go +++ b/examples/ping/v2/nats-jetstream/user/user.gen.go @@ -577,10 +577,10 @@ type PongMessageHeaders struct { // PongMessagePayload is a schema from the AsyncAPI specification required in messages type PongMessagePayload struct { // Description: Pong message - Message string `json:"message" validate:"required"` + Message string `json:"message"` // Description: Pong creation time - Time time.Time `json:"time" validate:"required"` + Time time.Time `json:"time"` } // PongMessage is the message expected for 'PongMessage' channel. diff --git a/examples/ping/v2/nats/app/app.gen.go b/examples/ping/v2/nats/app/app.gen.go index 345f49c..0f81a15 100644 --- a/examples/ping/v2/nats/app/app.gen.go +++ b/examples/ping/v2/nats/app/app.gen.go @@ -462,10 +462,10 @@ type PongMessageHeaders struct { // PongMessagePayload is a schema from the AsyncAPI specification required in messages type PongMessagePayload struct { // Description: Pong message - Message string `json:"message" validate:"required"` + Message string `json:"message"` // Description: Pong creation time - Time time.Time `json:"time" validate:"required"` + Time time.Time `json:"time"` } // PongMessage is the message expected for 'PongMessage' channel. diff --git a/examples/ping/v2/nats/user/user.gen.go b/examples/ping/v2/nats/user/user.gen.go index 9a010aa..9fd25ba 100644 --- a/examples/ping/v2/nats/user/user.gen.go +++ b/examples/ping/v2/nats/user/user.gen.go @@ -577,10 +577,10 @@ type PongMessageHeaders struct { // PongMessagePayload is a schema from the AsyncAPI specification required in messages type PongMessagePayload struct { // Description: Pong message - Message string `json:"message" validate:"required"` + Message string `json:"message"` // Description: Pong creation time - Time time.Time `json:"time" validate:"required"` + Time time.Time `json:"time"` } // PongMessage is the message expected for 'PongMessage' channel. diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index a9df351..84f65f4 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -10,7 +10,9 @@ import ( asyncapiv2 "github.com/lerenn/asyncapi-codegen/pkg/asyncapi/v2" asyncapiv3 "github.com/lerenn/asyncapi-codegen/pkg/asyncapi/v3" generatorv2 "github.com/lerenn/asyncapi-codegen/pkg/codegen/generators/v2" + templatesv2 "github.com/lerenn/asyncapi-codegen/pkg/codegen/generators/v2/templates" generatorv3 "github.com/lerenn/asyncapi-codegen/pkg/codegen/generators/v3" + templatesv3 "github.com/lerenn/asyncapi-codegen/pkg/codegen/generators/v3/templates" "github.com/lerenn/asyncapi-codegen/pkg/codegen/options" "github.com/lerenn/asyncapi-codegen/pkg/utils/template" "golang.org/x/tools/imports" @@ -91,6 +93,10 @@ func (cg CodeGen) Generate(opt options.Options) error { if opt.IgnoreStringFormat { template.DisableDateOrTimeGeneration() } + if opt.ForcePointers { + templatesv2.ForcePointerOnFields() + templatesv3.ForcePointerOnFields() + } // Process specification if err := cg.specification.Process(); err != nil { diff --git a/pkg/codegen/generators/helpers.go b/pkg/codegen/generators/helpers.go index aea9795..95e3207 100644 --- a/pkg/codegen/generators/helpers.go +++ b/pkg/codegen/generators/helpers.go @@ -30,9 +30,9 @@ func GenerateJSONTags[T any](schema asyncapi.Validations[T], field string) strin // GenerateValidateTags returns the "validate" tag for a given field in a struct, based on the asyncapi contract. // This tag can then be used by go-playground/validator/v10 to validate the struct's content. -func GenerateValidateTags[T any](schema asyncapi.Validations[T]) string { +func GenerateValidateTags[T any](schema asyncapi.Validations[T], isPointer bool, schemaType string) string { var directives []string - if schema.IsRequired { + if schema.IsRequired && (isPointer || schemaType == "array") { directives = append(directives, "required") } diff --git a/pkg/codegen/generators/v2/templates/helpers.go b/pkg/codegen/generators/v2/templates/helpers.go index 0322448..7d5d284 100644 --- a/pkg/codegen/generators/v2/templates/helpers.go +++ b/pkg/codegen/generators/v2/templates/helpers.go @@ -141,6 +141,17 @@ func OperationName(channel asyncapi.Channel) string { return templateutil.Namify(name) } +var isFieldPointer = func(parent asyncapi.Schema, field string, schema asyncapi.Schema) bool { + return !IsRequired(parent, field) && schema.Type != "array" +} + +// ForcePointerOnFields is used to force the generation of all fields as pointers, except for arrays. +func ForcePointerOnFields() { + isFieldPointer = func(parent asyncapi.Schema, field string, schema asyncapi.Schema) bool { + return schema.Type != "array" + } +} + // HelpersFunctions returns the functions that can be used as helpers // in a golang template. func HelpersFunctions() template.FuncMap { @@ -148,6 +159,7 @@ func HelpersFunctions() template.FuncMap { "getChildrenObjectSchemas": GetChildrenObjectSchemas, "channelToMessage": ChannelToMessage, "isRequired": IsRequired, + "isFieldPointer": isFieldPointer, "generateChannelPath": GenerateChannelPath, "referenceToStructAttributePath": ReferenceToStructAttributePath, "operationName": OperationName, diff --git a/pkg/codegen/generators/v2/templates/schema_definition.tmpl b/pkg/codegen/generators/v2/templates/schema_definition.tmpl index e37d7c2..eab7057 100644 --- a/pkg/codegen/generators/v2/templates/schema_definition.tmpl +++ b/pkg/codegen/generators/v2/templates/schema_definition.tmpl @@ -15,7 +15,7 @@ type {{ namify .Name }} struct { {{else if and $value.ReferenceTo $value.ReferenceTo.Description}} // Description: {{multiLineComment $value.ReferenceTo.Description}} {{end -}} - {{namify $key}} {{if and (not (isRequired $ $key)) (ne $value.Type "array")}}*{{end}}{{template "schema-name" $value}} `{{generateJSONTags $value.Validations $key}}{{generateValidateTags $value.Validations}}` + {{namify $key}} {{if isFieldPointer $ $key $value }}*{{end}}{{template "schema-name" $value}} `{{generateJSONTags $value.Validations $key}}{{generateValidateTags $value.Validations (isFieldPointer $ $key $value) $value.Type }}` {{end -}} {{- if .AdditionalProperties}} diff --git a/pkg/codegen/generators/v3/templates/helpers.go b/pkg/codegen/generators/v3/templates/helpers.go index 34e5067..6df5e39 100644 --- a/pkg/codegen/generators/v3/templates/helpers.go +++ b/pkg/codegen/generators/v3/templates/helpers.go @@ -127,6 +127,17 @@ func GenerateChannelAddr(ch *asyncapi.Channel) string { return sprint[:len(sprint)-1] + ")" } +var isFieldPointer = func(parent asyncapi.Schema, field string, schema asyncapi.Schema) bool { + return !IsRequired(parent, field) && schema.Type != "array" +} + +// ForcePointerOnFields is used to force the generation of all fields as pointers, except for arrays. +func ForcePointerOnFields() { + isFieldPointer = func(parent asyncapi.Schema, field string, schema asyncapi.Schema) bool { + return schema.Type != "array" + } +} + // HelpersFunctions returns the functions that can be used as helpers // in a golang template. func HelpersFunctions() template.FuncMap { @@ -136,6 +147,7 @@ func HelpersFunctions() template.FuncMap { "opToMsgTypeName": OpToMsgTypeName, "opToChannelTypeName": OpToChannelTypeName, "isRequired": IsRequired, + "isFieldPointer": isFieldPointer, "generateChannelAddr": GenerateChannelAddr, "generateChannelAddrFromOp": GenerateChannelAddrFromOp, "referenceToStructAttributePath": ReferenceToStructAttributePath, diff --git a/pkg/codegen/generators/v3/templates/schema_definition.tmpl b/pkg/codegen/generators/v3/templates/schema_definition.tmpl index e37d7c2..eab7057 100644 --- a/pkg/codegen/generators/v3/templates/schema_definition.tmpl +++ b/pkg/codegen/generators/v3/templates/schema_definition.tmpl @@ -15,7 +15,7 @@ type {{ namify .Name }} struct { {{else if and $value.ReferenceTo $value.ReferenceTo.Description}} // Description: {{multiLineComment $value.ReferenceTo.Description}} {{end -}} - {{namify $key}} {{if and (not (isRequired $ $key)) (ne $value.Type "array")}}*{{end}}{{template "schema-name" $value}} `{{generateJSONTags $value.Validations $key}}{{generateValidateTags $value.Validations}}` + {{namify $key}} {{if isFieldPointer $ $key $value }}*{{end}}{{template "schema-name" $value}} `{{generateJSONTags $value.Validations $key}}{{generateValidateTags $value.Validations (isFieldPointer $ $key $value) $value.Type }}` {{end -}} {{- if .AdditionalProperties}} diff --git a/pkg/codegen/options/options.go b/pkg/codegen/options/options.go index c44466b..06564a8 100644 --- a/pkg/codegen/options/options.go +++ b/pkg/codegen/options/options.go @@ -35,4 +35,7 @@ type Options struct { // IgnoreStringFormat states whether the properties' format (date, date-time) should impact the type in types IgnoreStringFormat bool + + // ForcePointers can be used to force all struct fields to be generated as pointers + ForcePointers bool } diff --git a/test/v2/issues/131/asyncapi.gen.go b/test/v2/issues/131/asyncapi.gen.go index d466835..4816c1e 100644 --- a/test/v2/issues/131/asyncapi.gen.go +++ b/test/v2/issues/131/asyncapi.gen.go @@ -499,7 +499,7 @@ type TestSchema struct { FloatProp *float64 `json:"FloatProp,omitempty" validate:"omitempty,gte=2.5,lte=5.5"` IntegerExclusiveProp *int64 `json:"IntegerExclusiveProp,omitempty" validate:"omitempty,gt=2,lt=5"` IntegerProp *int64 `json:"IntegerProp,omitempty" validate:"omitempty,gte=2,lte=5"` - RequiredProp string `json:"RequiredProp" validate:"required"` + RequiredProp string `json:"RequiredProp"` StringProp *string `json:"StringProp,omitempty" validate:"omitempty,min=2,max=5"` } diff --git a/test/v2/issues/131/suite_test.go b/test/v2/issues/131/suite_test.go index 9836781..f87e8ce 100644 --- a/test/v2/issues/131/suite_test.go +++ b/test/v2/issues/131/suite_test.go @@ -92,13 +92,6 @@ func (suite *Suite) TestFloat() { assert.Error(suite.T(), validator.New().Struct(tooLarge)) } -func (suite *Suite) TestRequired() { - invalidAbsent := ValidTestSchema() - invalidAbsent.RequiredProp = "" - - assert.Error(suite.T(), validator.New().Struct(invalidAbsent)) -} - func (suite *Suite) TestArray() { empty := ValidTestSchema() empty.ArrayProp = []string{} diff --git a/test/v2/issues/185/asyncapi.gen.go b/test/v2/issues/185/asyncapi.gen.go index d87d5ad..48e5824 100644 --- a/test/v2/issues/185/asyncapi.gen.go +++ b/test/v2/issues/185/asyncapi.gen.go @@ -70,22 +70,22 @@ func (e *Error) Error() string { // BaseEventSchema is a schema from the AsyncAPI specification required in messages type BaseEventSchema struct { - Time time.Time `json:"time" validate:"required"` + Time time.Time `json:"time"` } // BaseEventInfoSchema is a schema from the AsyncAPI specification required in messages type BaseEventInfoSchema struct { - Data ContentDataSchema `json:"data" validate:"required"` + Data ContentDataSchema `json:"data"` } // ContentDataSchema is a schema from the AsyncAPI specification required in messages type ContentDataSchema struct { - ContentId string `json:"contentId" validate:"required"` + ContentId string `json:"contentId"` } // EventPayloadSchema is a schema from the AsyncAPI specification required in messages type EventPayloadSchema struct { - Data ContentDataSchema `json:"data" validate:"required"` - Id string `json:"id" validate:"required"` - Time time.Time `json:"time" validate:"required"` + Data ContentDataSchema `json:"data"` + Id string `json:"id"` + Time time.Time `json:"time"` } diff --git a/test/v2/issues/245/asyncapi.gen.go b/test/v2/issues/245/asyncapi.gen.go index 7c9bd37..28f2e88 100644 --- a/test/v2/issues/245/asyncapi.gen.go +++ b/test/v2/issues/245/asyncapi.gen.go @@ -498,7 +498,7 @@ type TestSchema struct { EnumProp *string `json:"EnumProp,omitempty" validate:"omitempty,oneof=red amber green"` FloatProp *float64 `json:"FloatProp,omitempty" validate:"omitempty,gte=2.5,lte=5.5"` IntegerProp *int64 `json:"IntegerProp,omitempty" validate:"omitempty,gte=2,lte=5"` - RequiredProp string `json:"RequiredProp" validate:"required"` + RequiredProp string `json:"RequiredProp"` StringProp *string `json:"StringProp,omitempty" validate:"omitempty,min=2,max=5"` } diff --git a/test/v2/issues/259/asyncapi.yaml b/test/v2/issues/259/asyncapi.yaml new file mode 100644 index 0000000..b714467 --- /dev/null +++ b/test/v2/issues/259/asyncapi.yaml @@ -0,0 +1,27 @@ +asyncapi: 2.6.0 +info: + title: Sample App + version: 1.2.3 + +components: + messages: + Test: + payload: + type: object + required: + - reqField + - reqArray + properties: + reqField: + type: string + nonReqField: + type: string + reqArray: + type: array + items: + type: string + nonReqArray: + type: array + items: + type: string + diff --git a/test/v2/issues/259/default/asyncapi.gen.go b/test/v2/issues/259/default/asyncapi.gen.go new file mode 100644 index 0000000..4c8dcaa --- /dev/null +++ b/test/v2/issues/259/default/asyncapi.gen.go @@ -0,0 +1,320 @@ +// Package "issue259default" provides primitives to interact with the AsyncAPI specification. +// +// Code generated by github.com/lerenn/asyncapi-codegen version (devel) DO NOT EDIT. +package issue259default + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/lerenn/asyncapi-codegen/pkg/extensions" +) + +// AppController is the structure that provides publishing capabilities to the +// developer and and connect the broker with the App +type AppController struct { + controller +} + +// NewAppController links the App to the broker +func NewAppController(bc extensions.BrokerController, options ...ControllerOption) (*AppController, error) { + // Check if broker controller has been provided + if bc == nil { + return nil, extensions.ErrNilBrokerController + } + + // Create default controller + controller := controller{ + broker: bc, + subscriptions: make(map[string]extensions.BrokerChannelSubscription), + logger: extensions.DummyLogger{}, + middlewares: make([]extensions.Middleware, 0), + errorHandler: extensions.DefaultErrorHandler(), + } + + // Apply options + for _, option := range options { + option(&controller) + } + + return &AppController{controller: controller}, nil +} + +func (c AppController) wrapMiddlewares( + middlewares []extensions.Middleware, + callback extensions.NextMiddleware, +) func(ctx context.Context, msg *extensions.BrokerMessage) error { + var called bool + + // If there is no more middleware + if len(middlewares) == 0 { + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the callback if it exists and it has not been called already + if callback != nil && !called { + called = true + return callback(ctx) + } + + // Nil can be returned, as the callback has already been called + return nil + } + } + + // Get the next function to call from next middlewares or callback + next := c.wrapMiddlewares(middlewares[1:], callback) + + // Wrap middleware into a check function that will call execute the middleware + // and call the next wrapped middleware if the returned function has not been + // called already + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the middleware and the following if it has not been done already + if !called { + // Create the next call with the context and the message + nextWithArgs := func(ctx context.Context) error { + return next(ctx, msg) + } + + // Call the middleware and register it as already called + called = true + if err := middlewares[0](ctx, msg, nextWithArgs); err != nil { + return err + } + + // If next has already been called in middleware, it should not be executed again + return nextWithArgs(ctx) + } + + // Nil can be returned, as the next middleware has already been called + return nil + } +} + +func (c AppController) executeMiddlewares(ctx context.Context, msg *extensions.BrokerMessage, callback extensions.NextMiddleware) error { + // Wrap middleware to have 'next' function when calling them + wrapped := c.wrapMiddlewares(c.middlewares, callback) + + // Execute wrapped middlewares + return wrapped(ctx, msg) +} + +func addAppContextValues(ctx context.Context, path string) context.Context { + ctx = context.WithValue(ctx, extensions.ContextKeyIsVersion, "1.2.3") + ctx = context.WithValue(ctx, extensions.ContextKeyIsProvider, "app") + return context.WithValue(ctx, extensions.ContextKeyIsChannel, path) +} + +// Close will clean up any existing resources on the controller +func (c *AppController) Close(ctx context.Context) { + // Unsubscribing remaining channels +} + +// UserController is the structure that provides publishing capabilities to the +// developer and and connect the broker with the User +type UserController struct { + controller +} + +// NewUserController links the User to the broker +func NewUserController(bc extensions.BrokerController, options ...ControllerOption) (*UserController, error) { + // Check if broker controller has been provided + if bc == nil { + return nil, extensions.ErrNilBrokerController + } + + // Create default controller + controller := controller{ + broker: bc, + subscriptions: make(map[string]extensions.BrokerChannelSubscription), + logger: extensions.DummyLogger{}, + middlewares: make([]extensions.Middleware, 0), + errorHandler: extensions.DefaultErrorHandler(), + } + + // Apply options + for _, option := range options { + option(&controller) + } + + return &UserController{controller: controller}, nil +} + +func (c UserController) wrapMiddlewares( + middlewares []extensions.Middleware, + callback extensions.NextMiddleware, +) func(ctx context.Context, msg *extensions.BrokerMessage) error { + var called bool + + // If there is no more middleware + if len(middlewares) == 0 { + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the callback if it exists and it has not been called already + if callback != nil && !called { + called = true + return callback(ctx) + } + + // Nil can be returned, as the callback has already been called + return nil + } + } + + // Get the next function to call from next middlewares or callback + next := c.wrapMiddlewares(middlewares[1:], callback) + + // Wrap middleware into a check function that will call execute the middleware + // and call the next wrapped middleware if the returned function has not been + // called already + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the middleware and the following if it has not been done already + if !called { + // Create the next call with the context and the message + nextWithArgs := func(ctx context.Context) error { + return next(ctx, msg) + } + + // Call the middleware and register it as already called + called = true + if err := middlewares[0](ctx, msg, nextWithArgs); err != nil { + return err + } + + // If next has already been called in middleware, it should not be executed again + return nextWithArgs(ctx) + } + + // Nil can be returned, as the next middleware has already been called + return nil + } +} + +func (c UserController) executeMiddlewares(ctx context.Context, msg *extensions.BrokerMessage, callback extensions.NextMiddleware) error { + // Wrap middleware to have 'next' function when calling them + wrapped := c.wrapMiddlewares(c.middlewares, callback) + + // Execute wrapped middlewares + return wrapped(ctx, msg) +} + +func addUserContextValues(ctx context.Context, path string) context.Context { + ctx = context.WithValue(ctx, extensions.ContextKeyIsVersion, "1.2.3") + ctx = context.WithValue(ctx, extensions.ContextKeyIsProvider, "user") + return context.WithValue(ctx, extensions.ContextKeyIsChannel, path) +} + +// Close will clean up any existing resources on the controller +func (c *UserController) Close(ctx context.Context) { + // Unsubscribing remaining channels +} + +// AsyncAPIVersion is the version of the used AsyncAPI document +const AsyncAPIVersion = "1.2.3" + +// controller is the controller that will be used to communicate with the broker +// It will be used internally by AppController and UserController +type controller struct { + // broker is the broker controller that will be used to communicate + broker extensions.BrokerController + // subscriptions is a map of all subscriptions + subscriptions map[string]extensions.BrokerChannelSubscription + // logger is the logger that will be used² to log operations on controller + logger extensions.Logger + // middlewares are the middlewares that will be executed when sending or + // receiving messages + middlewares []extensions.Middleware + // handler to handle errors from consumers and middlewares + errorHandler extensions.ErrorHandler +} + +// ControllerOption is the type of the options that can be passed +// when creating a new Controller +type ControllerOption func(controller *controller) + +// WithLogger attaches a logger to the controller +func WithLogger(logger extensions.Logger) ControllerOption { + return func(controller *controller) { + controller.logger = logger + } +} + +// WithMiddlewares attaches middlewares that will be executed when sending or receiving messages +func WithMiddlewares(middlewares ...extensions.Middleware) ControllerOption { + return func(controller *controller) { + controller.middlewares = middlewares + } +} + +// WithErrorHandler attaches a errorhandler to handle errors from subscriber functions +func WithErrorHandler(handler extensions.ErrorHandler) ControllerOption { + return func(controller *controller) { + controller.errorHandler = handler + } +} + +type MessageWithCorrelationID interface { + CorrelationID() string + SetCorrelationID(id string) +} + +type Error struct { + Channel string + Err error +} + +func (e *Error) Error() string { + return fmt.Sprintf("channel %q: err %v", e.Channel, e.Err) +} + +// TestMessagePayload is a schema from the AsyncAPI specification required in messages +type TestMessagePayload struct { + NonReqArray []string `json:"nonReqArray,omitempty"` + NonReqField *string `json:"nonReqField,omitempty"` + ReqArray []string `json:"reqArray" validate:"required"` + ReqField string `json:"reqField"` +} + +// TestMessage is the message expected for 'TestMessage' channel. +type TestMessage struct { + // Payload will be inserted in the message payload + Payload TestMessagePayload +} + +func NewTestMessage() TestMessage { + var msg TestMessage + + return msg +} + +// brokerMessageToTestMessage will fill a new TestMessage with data from generic broker message +func brokerMessageToTestMessage(bMsg extensions.BrokerMessage) (TestMessage, error) { + var msg TestMessage + + // Unmarshal payload to expected message payload format + err := json.Unmarshal(bMsg.Payload, &msg.Payload) + if err != nil { + return msg, err + } + + // TODO: run checks on msg type + + return msg, nil +} + +// toBrokerMessage will generate a generic broker message from TestMessage data +func (msg TestMessage) toBrokerMessage() (extensions.BrokerMessage, error) { + // TODO: implement checks on message + + // Marshal payload to JSON + payload, err := json.Marshal(msg.Payload) + if err != nil { + return extensions.BrokerMessage{}, err + } + + // There is no headers here + headers := make(map[string][]byte, 0) + + return extensions.BrokerMessage{ + Headers: headers, + Payload: payload, + }, nil +} diff --git a/test/v2/issues/259/forcepointers/asyncapi.gen.go b/test/v2/issues/259/forcepointers/asyncapi.gen.go new file mode 100644 index 0000000..9258a7f --- /dev/null +++ b/test/v2/issues/259/forcepointers/asyncapi.gen.go @@ -0,0 +1,320 @@ +// Package "issue259forcepointers" provides primitives to interact with the AsyncAPI specification. +// +// Code generated by github.com/lerenn/asyncapi-codegen version (devel) DO NOT EDIT. +package issue259forcepointers + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/lerenn/asyncapi-codegen/pkg/extensions" +) + +// AppController is the structure that provides publishing capabilities to the +// developer and and connect the broker with the App +type AppController struct { + controller +} + +// NewAppController links the App to the broker +func NewAppController(bc extensions.BrokerController, options ...ControllerOption) (*AppController, error) { + // Check if broker controller has been provided + if bc == nil { + return nil, extensions.ErrNilBrokerController + } + + // Create default controller + controller := controller{ + broker: bc, + subscriptions: make(map[string]extensions.BrokerChannelSubscription), + logger: extensions.DummyLogger{}, + middlewares: make([]extensions.Middleware, 0), + errorHandler: extensions.DefaultErrorHandler(), + } + + // Apply options + for _, option := range options { + option(&controller) + } + + return &AppController{controller: controller}, nil +} + +func (c AppController) wrapMiddlewares( + middlewares []extensions.Middleware, + callback extensions.NextMiddleware, +) func(ctx context.Context, msg *extensions.BrokerMessage) error { + var called bool + + // If there is no more middleware + if len(middlewares) == 0 { + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the callback if it exists and it has not been called already + if callback != nil && !called { + called = true + return callback(ctx) + } + + // Nil can be returned, as the callback has already been called + return nil + } + } + + // Get the next function to call from next middlewares or callback + next := c.wrapMiddlewares(middlewares[1:], callback) + + // Wrap middleware into a check function that will call execute the middleware + // and call the next wrapped middleware if the returned function has not been + // called already + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the middleware and the following if it has not been done already + if !called { + // Create the next call with the context and the message + nextWithArgs := func(ctx context.Context) error { + return next(ctx, msg) + } + + // Call the middleware and register it as already called + called = true + if err := middlewares[0](ctx, msg, nextWithArgs); err != nil { + return err + } + + // If next has already been called in middleware, it should not be executed again + return nextWithArgs(ctx) + } + + // Nil can be returned, as the next middleware has already been called + return nil + } +} + +func (c AppController) executeMiddlewares(ctx context.Context, msg *extensions.BrokerMessage, callback extensions.NextMiddleware) error { + // Wrap middleware to have 'next' function when calling them + wrapped := c.wrapMiddlewares(c.middlewares, callback) + + // Execute wrapped middlewares + return wrapped(ctx, msg) +} + +func addAppContextValues(ctx context.Context, path string) context.Context { + ctx = context.WithValue(ctx, extensions.ContextKeyIsVersion, "1.2.3") + ctx = context.WithValue(ctx, extensions.ContextKeyIsProvider, "app") + return context.WithValue(ctx, extensions.ContextKeyIsChannel, path) +} + +// Close will clean up any existing resources on the controller +func (c *AppController) Close(ctx context.Context) { + // Unsubscribing remaining channels +} + +// UserController is the structure that provides publishing capabilities to the +// developer and and connect the broker with the User +type UserController struct { + controller +} + +// NewUserController links the User to the broker +func NewUserController(bc extensions.BrokerController, options ...ControllerOption) (*UserController, error) { + // Check if broker controller has been provided + if bc == nil { + return nil, extensions.ErrNilBrokerController + } + + // Create default controller + controller := controller{ + broker: bc, + subscriptions: make(map[string]extensions.BrokerChannelSubscription), + logger: extensions.DummyLogger{}, + middlewares: make([]extensions.Middleware, 0), + errorHandler: extensions.DefaultErrorHandler(), + } + + // Apply options + for _, option := range options { + option(&controller) + } + + return &UserController{controller: controller}, nil +} + +func (c UserController) wrapMiddlewares( + middlewares []extensions.Middleware, + callback extensions.NextMiddleware, +) func(ctx context.Context, msg *extensions.BrokerMessage) error { + var called bool + + // If there is no more middleware + if len(middlewares) == 0 { + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the callback if it exists and it has not been called already + if callback != nil && !called { + called = true + return callback(ctx) + } + + // Nil can be returned, as the callback has already been called + return nil + } + } + + // Get the next function to call from next middlewares or callback + next := c.wrapMiddlewares(middlewares[1:], callback) + + // Wrap middleware into a check function that will call execute the middleware + // and call the next wrapped middleware if the returned function has not been + // called already + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the middleware and the following if it has not been done already + if !called { + // Create the next call with the context and the message + nextWithArgs := func(ctx context.Context) error { + return next(ctx, msg) + } + + // Call the middleware and register it as already called + called = true + if err := middlewares[0](ctx, msg, nextWithArgs); err != nil { + return err + } + + // If next has already been called in middleware, it should not be executed again + return nextWithArgs(ctx) + } + + // Nil can be returned, as the next middleware has already been called + return nil + } +} + +func (c UserController) executeMiddlewares(ctx context.Context, msg *extensions.BrokerMessage, callback extensions.NextMiddleware) error { + // Wrap middleware to have 'next' function when calling them + wrapped := c.wrapMiddlewares(c.middlewares, callback) + + // Execute wrapped middlewares + return wrapped(ctx, msg) +} + +func addUserContextValues(ctx context.Context, path string) context.Context { + ctx = context.WithValue(ctx, extensions.ContextKeyIsVersion, "1.2.3") + ctx = context.WithValue(ctx, extensions.ContextKeyIsProvider, "user") + return context.WithValue(ctx, extensions.ContextKeyIsChannel, path) +} + +// Close will clean up any existing resources on the controller +func (c *UserController) Close(ctx context.Context) { + // Unsubscribing remaining channels +} + +// AsyncAPIVersion is the version of the used AsyncAPI document +const AsyncAPIVersion = "1.2.3" + +// controller is the controller that will be used to communicate with the broker +// It will be used internally by AppController and UserController +type controller struct { + // broker is the broker controller that will be used to communicate + broker extensions.BrokerController + // subscriptions is a map of all subscriptions + subscriptions map[string]extensions.BrokerChannelSubscription + // logger is the logger that will be used² to log operations on controller + logger extensions.Logger + // middlewares are the middlewares that will be executed when sending or + // receiving messages + middlewares []extensions.Middleware + // handler to handle errors from consumers and middlewares + errorHandler extensions.ErrorHandler +} + +// ControllerOption is the type of the options that can be passed +// when creating a new Controller +type ControllerOption func(controller *controller) + +// WithLogger attaches a logger to the controller +func WithLogger(logger extensions.Logger) ControllerOption { + return func(controller *controller) { + controller.logger = logger + } +} + +// WithMiddlewares attaches middlewares that will be executed when sending or receiving messages +func WithMiddlewares(middlewares ...extensions.Middleware) ControllerOption { + return func(controller *controller) { + controller.middlewares = middlewares + } +} + +// WithErrorHandler attaches a errorhandler to handle errors from subscriber functions +func WithErrorHandler(handler extensions.ErrorHandler) ControllerOption { + return func(controller *controller) { + controller.errorHandler = handler + } +} + +type MessageWithCorrelationID interface { + CorrelationID() string + SetCorrelationID(id string) +} + +type Error struct { + Channel string + Err error +} + +func (e *Error) Error() string { + return fmt.Sprintf("channel %q: err %v", e.Channel, e.Err) +} + +// TestMessagePayload is a schema from the AsyncAPI specification required in messages +type TestMessagePayload struct { + NonReqArray []string `json:"nonReqArray,omitempty"` + NonReqField *string `json:"nonReqField,omitempty"` + ReqArray []string `json:"reqArray" validate:"required"` + ReqField *string `json:"reqField" validate:"required"` +} + +// TestMessage is the message expected for 'TestMessage' channel. +type TestMessage struct { + // Payload will be inserted in the message payload + Payload TestMessagePayload +} + +func NewTestMessage() TestMessage { + var msg TestMessage + + return msg +} + +// brokerMessageToTestMessage will fill a new TestMessage with data from generic broker message +func brokerMessageToTestMessage(bMsg extensions.BrokerMessage) (TestMessage, error) { + var msg TestMessage + + // Unmarshal payload to expected message payload format + err := json.Unmarshal(bMsg.Payload, &msg.Payload) + if err != nil { + return msg, err + } + + // TODO: run checks on msg type + + return msg, nil +} + +// toBrokerMessage will generate a generic broker message from TestMessage data +func (msg TestMessage) toBrokerMessage() (extensions.BrokerMessage, error) { + // TODO: implement checks on message + + // Marshal payload to JSON + payload, err := json.Marshal(msg.Payload) + if err != nil { + return extensions.BrokerMessage{}, err + } + + // There is no headers here + headers := make(map[string][]byte, 0) + + return extensions.BrokerMessage{ + Headers: headers, + Payload: payload, + }, nil +} diff --git a/test/v2/issues/259/suite_test.go b/test/v2/issues/259/suite_test.go new file mode 100644 index 0000000..dda3d3e --- /dev/null +++ b/test/v2/issues/259/suite_test.go @@ -0,0 +1,106 @@ +//go:generate go run ../../../../cmd/asyncapi-codegen -p issue259default -i ./asyncapi.yaml -o ./default/asyncapi.gen.go +//go:generate go run ../../../../cmd/asyncapi-codegen -p issue259forcepointers --force-pointers -i ./asyncapi.yaml -o ./forcepointers/asyncapi.gen.go + +package issue259 + +import ( + "encoding/json" + "testing" + + "github.com/go-playground/validator/v10" + issue259default "github.com/lerenn/asyncapi-codegen/test/v2/issues/259/default" + issue259forcepointers "github.com/lerenn/asyncapi-codegen/test/v2/issues/259/forcepointers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +func TestSuite(t *testing.T) { + suite.Run(t, NewSuite()) +} + +type Suite struct { + suite.Suite +} + +func NewSuite() *Suite { + return &Suite{} +} + +func (suite *Suite) TestDefault() { + validate := validator.New() + + // Required absent - no error due to empty/nil fields + assert.NoError(suite.T(), validate.Struct( + issue259default.TestMessagePayload{ + NonReqArray: nil, + NonReqField: nil, + ReqArray: []string{}, + ReqField: "", + }, + )) + + var out issue259default.TestMessagePayload + err := json.Unmarshal([]byte(`{}`), &out) + assert.NoError(suite.T(), err) +} + +func (suite *Suite) TestForcePointers() { + validate := validator.New() + + cases := []struct { + name string + inJSON string + + isErrExpected bool + }{ + { + name: "empty input", + inJSON: `{}`, + isErrExpected: true, + }, + { + name: "full input", + inJSON: `{"reqField": "something","nonReqField": "something","reqArray": ["something"], +"nonReqArray": ["something"]}`, + isErrExpected: false, + }, + { + name: "only required fields", + inJSON: `{"reqField": "something","reqArray": ["something"]}`, + isErrExpected: false, + }, + { + name: "empty array", + inJSON: `{"reqField": "something","reqArray": []}`, + isErrExpected: false, + }, + { + name: "empty string", + inJSON: `{"reqField": "","reqArray": ["something"]}`, + isErrExpected: false, + }, + { + name: "missing string", + inJSON: `{"reqArray": ["something"]}`, + isErrExpected: true, + }, + { + name: "missing array", + inJSON: `{"reqField": "something"}`, + isErrExpected: true, + }, + } + + for _, tc := range cases { + suite.T().Run(tc.name, func(t *testing.T) { + var out issue259forcepointers.TestMessagePayload + assert.NoError(t, json.Unmarshal([]byte(tc.inJSON), &out)) + + if tc.isErrExpected { + assert.Error(t, validate.Struct(out)) + } else { + assert.NoError(t, validate.Struct(out)) + } + }) + } +} diff --git a/test/v2/issues/73/suite_test.go b/test/v2/issues/73/suite_test.go index af8e288..cc3c141 100644 --- a/test/v2/issues/73/suite_test.go +++ b/test/v2/issues/73/suite_test.go @@ -139,8 +139,8 @@ func (suite *Suite) TestV2Reception() { // Expected message sent := v2.V2Issue73HelloMessage{ Payload: struct { - Message string `json:"message" validate:"required"` - Timestamp time.Time `json:"timestamp" validate:"required"` + Message string `json:"message"` + Timestamp time.Time `json:"timestamp"` }{ Message: "HelloWord!", Timestamp: utils.Must(time.Parse(time.RFC3339, "2020-01-01T00:00:00Z")).UTC(), diff --git a/test/v2/issues/73/v2/asyncapi.gen.go b/test/v2/issues/73/v2/asyncapi.gen.go index 698548f..c8b9726 100644 --- a/test/v2/issues/73/v2/asyncapi.gen.go +++ b/test/v2/issues/73/v2/asyncapi.gen.go @@ -448,8 +448,8 @@ func (e *Error) Error() string { // V2Issue73HelloMessagePayload is a schema from the AsyncAPI specification required in messages type V2Issue73HelloMessagePayload struct { - Message string `json:"message" validate:"required"` - Timestamp time.Time `json:"timestamp" validate:"required"` + Message string `json:"message"` + Timestamp time.Time `json:"timestamp"` } // V2Issue73HelloMessage is the message expected for 'V2Issue73HelloMessage' channel. diff --git a/test/v2/issues/74/asyncapi.gen.go b/test/v2/issues/74/asyncapi.gen.go index 31d9597..9323442 100644 --- a/test/v2/issues/74/asyncapi.gen.go +++ b/test/v2/issues/74/asyncapi.gen.go @@ -448,7 +448,7 @@ func (e *Error) Error() string { // TestMessagePayload is a schema from the AsyncAPI specification required in messages type TestMessagePayload struct { - Obj1 TestSchemaObj1 `json:"obj1" validate:"required"` + Obj1 TestSchemaObj1 `json:"obj1"` } // TestMessage is the message expected for 'TestMessage' channel. @@ -525,21 +525,21 @@ func (msg TestMessage) toBrokerMessage() (extensions.BrokerMessage, error) { // Description: header type HeaderSchema struct { // Description: Date in UTC format "YYYY-MM-DDThh:mm:ss.sZ". - DateTime time.Time `json:"dateTime" validate:"required"` + DateTime time.Time `json:"dateTime"` // Description: Schema version - Version string `json:"version" validate:"required"` + Version string `json:"version"` } // TestSchema is a schema from the AsyncAPI specification required in messages type TestSchema struct { - Obj1 TestSchemaObj1 `json:"obj1" validate:"required"` + Obj1 TestSchemaObj1 `json:"obj1"` } // TestSchemaObj1 is a schema from the AsyncAPI specification required in messages type TestSchemaObj1 struct { // Description: reference ID. - ReferenceId string `json:"referenceId" validate:"required"` + ReferenceId string `json:"referenceId"` } const ( diff --git a/test/v3/issues/131/asyncapi.gen.go b/test/v3/issues/131/asyncapi.gen.go index 8b6e2db..9f13ac3 100644 --- a/test/v3/issues/131/asyncapi.gen.go +++ b/test/v3/issues/131/asyncapi.gen.go @@ -510,7 +510,7 @@ type TestSchema struct { FloatProp *float64 `json:"FloatProp,omitempty" validate:"omitempty,gte=2.5,lte=5.5"` IntegerExclusiveProp *int64 `json:"IntegerExclusiveProp,omitempty" validate:"omitempty,gt=2,lt=5"` IntegerProp *int64 `json:"IntegerProp,omitempty" validate:"omitempty,gte=2,lte=5"` - RequiredProp string `json:"RequiredProp" validate:"required"` + RequiredProp string `json:"RequiredProp"` StringProp *string `json:"StringProp,omitempty" validate:"omitempty,min=2,max=5"` } diff --git a/test/v3/issues/131/suite_test.go b/test/v3/issues/131/suite_test.go index e1b48da..6dbd606 100644 --- a/test/v3/issues/131/suite_test.go +++ b/test/v3/issues/131/suite_test.go @@ -121,13 +121,6 @@ func (suite *Suite) TestFloat() { assert.NoError(suite.T(), validator.New().Struct(validNil)) } -func (suite *Suite) TestRequired() { - invalidAbsent := ValidTestSchema() - invalidAbsent.RequiredProp = "" - - assert.Error(suite.T(), validator.New().Struct(invalidAbsent)) -} - func (suite *Suite) TestArray() { empty := ValidTestSchema() empty.ArrayProp = []string{} diff --git a/test/v3/issues/181/asyncapi.gen.go b/test/v3/issues/181/asyncapi.gen.go index 20a1ab0..7add19d 100644 --- a/test/v3/issues/181/asyncapi.gen.go +++ b/test/v3/issues/181/asyncapi.gen.go @@ -659,7 +659,7 @@ func (msg ReplyMessageFromReplyChannel) toBrokerMessage() (extensions.BrokerMess // HeadersFromRequestMessage is a schema from the AsyncAPI specification required in messages type HeadersFromRequestMessage struct { // Description: Channel used to respond to request - ReplyTo string `json:"replyTo" validate:"required"` + ReplyTo string `json:"replyTo"` } // RequestMessage is the message expected for 'RequestMessage' channel. diff --git a/test/v3/issues/185/asyncapi.gen.go b/test/v3/issues/185/asyncapi.gen.go index d87d5ad..48e5824 100644 --- a/test/v3/issues/185/asyncapi.gen.go +++ b/test/v3/issues/185/asyncapi.gen.go @@ -70,22 +70,22 @@ func (e *Error) Error() string { // BaseEventSchema is a schema from the AsyncAPI specification required in messages type BaseEventSchema struct { - Time time.Time `json:"time" validate:"required"` + Time time.Time `json:"time"` } // BaseEventInfoSchema is a schema from the AsyncAPI specification required in messages type BaseEventInfoSchema struct { - Data ContentDataSchema `json:"data" validate:"required"` + Data ContentDataSchema `json:"data"` } // ContentDataSchema is a schema from the AsyncAPI specification required in messages type ContentDataSchema struct { - ContentId string `json:"contentId" validate:"required"` + ContentId string `json:"contentId"` } // EventPayloadSchema is a schema from the AsyncAPI specification required in messages type EventPayloadSchema struct { - Data ContentDataSchema `json:"data" validate:"required"` - Id string `json:"id" validate:"required"` - Time time.Time `json:"time" validate:"required"` + Data ContentDataSchema `json:"data"` + Id string `json:"id"` + Time time.Time `json:"time"` } diff --git a/test/v3/issues/241/asyncapi.gen.go b/test/v3/issues/241/asyncapi.gen.go index d03b594..4bab477 100644 --- a/test/v3/issues/241/asyncapi.gen.go +++ b/test/v3/issues/241/asyncapi.gen.go @@ -267,7 +267,7 @@ func (e *Error) Error() string { // HeadersFromPingMessageFromTestChannel is a schema from the AsyncAPI specification required in messages type HeadersFromPingMessageFromTestChannel struct { - EventId EventIdSchema `json:"event_id" validate:"required"` + EventId EventIdSchema `json:"event_id"` OptionalEventId *EventIdSchema `json:"optional_event_id,omitempty"` } diff --git a/test/v3/issues/245/asyncapi.gen.go b/test/v3/issues/245/asyncapi.gen.go index d46ebe5..98a6c6c 100644 --- a/test/v3/issues/245/asyncapi.gen.go +++ b/test/v3/issues/245/asyncapi.gen.go @@ -509,7 +509,7 @@ type TestSchema struct { EnumProp *string `json:"EnumProp,omitempty" validate:"omitempty,oneof=red amber green"` FloatProp *float64 `json:"FloatProp,omitempty" validate:"omitempty,gte=2.5,lte=5.5"` IntegerProp *int64 `json:"IntegerProp,omitempty" validate:"omitempty,gte=2,lte=5"` - RequiredProp string `json:"RequiredProp" validate:"required"` + RequiredProp string `json:"RequiredProp"` StringProp *string `json:"StringProp,omitempty" validate:"omitempty,min=2,max=5"` } diff --git a/test/v3/issues/259/asyncapi.yaml b/test/v3/issues/259/asyncapi.yaml new file mode 100644 index 0000000..bf08739 --- /dev/null +++ b/test/v3/issues/259/asyncapi.yaml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Sample App + version: 1.2.3 + +components: + messages: + Test: + payload: + type: object + required: + - reqField + - reqArray + properties: + reqField: + type: string + nonReqField: + type: string + reqArray: + type: array + items: + type: string + nonReqArray: + type: array + items: + type: string + diff --git a/test/v3/issues/259/default/asyncapi.gen.go b/test/v3/issues/259/default/asyncapi.gen.go new file mode 100644 index 0000000..b225938 --- /dev/null +++ b/test/v3/issues/259/default/asyncapi.gen.go @@ -0,0 +1,320 @@ +// Package "issue259default" provides primitives to interact with the AsyncAPI specification. +// +// Code generated by github.com/lerenn/asyncapi-codegen version (devel) DO NOT EDIT. +package issue259default + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/lerenn/asyncapi-codegen/pkg/extensions" +) + +// AppController is the structure that provides sending capabilities to the +// developer and and connect the broker with the App +type AppController struct { + controller +} + +// NewAppController links the App to the broker +func NewAppController(bc extensions.BrokerController, options ...ControllerOption) (*AppController, error) { + // Check if broker controller has been provided + if bc == nil { + return nil, extensions.ErrNilBrokerController + } + + // Create default controller + controller := controller{ + broker: bc, + subscriptions: make(map[string]extensions.BrokerChannelSubscription), + logger: extensions.DummyLogger{}, + middlewares: make([]extensions.Middleware, 0), + errorHandler: extensions.DefaultErrorHandler(), + } + + // Apply options + for _, option := range options { + option(&controller) + } + + return &AppController{controller: controller}, nil +} + +func (c AppController) wrapMiddlewares( + middlewares []extensions.Middleware, + callback extensions.NextMiddleware, +) func(ctx context.Context, msg *extensions.BrokerMessage) error { + var called bool + + // If there is no more middleware + if len(middlewares) == 0 { + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the callback if it exists and it has not been called already + if callback != nil && !called { + called = true + return callback(ctx) + } + + // Nil can be returned, as the callback has already been called + return nil + } + } + + // Get the next function to call from next middlewares or callback + next := c.wrapMiddlewares(middlewares[1:], callback) + + // Wrap middleware into a check function that will call execute the middleware + // and call the next wrapped middleware if the returned function has not been + // called already + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the middleware and the following if it has not been done already + if !called { + // Create the next call with the context and the message + nextWithArgs := func(ctx context.Context) error { + return next(ctx, msg) + } + + // Call the middleware and register it as already called + called = true + if err := middlewares[0](ctx, msg, nextWithArgs); err != nil { + return err + } + + // If next has already been called in middleware, it should not be executed again + return nextWithArgs(ctx) + } + + // Nil can be returned, as the next middleware has already been called + return nil + } +} + +func (c AppController) executeMiddlewares(ctx context.Context, msg *extensions.BrokerMessage, callback extensions.NextMiddleware) error { + // Wrap middleware to have 'next' function when calling them + wrapped := c.wrapMiddlewares(c.middlewares, callback) + + // Execute wrapped middlewares + return wrapped(ctx, msg) +} + +func addAppContextValues(ctx context.Context, addr string) context.Context { + ctx = context.WithValue(ctx, extensions.ContextKeyIsVersion, "1.2.3") + ctx = context.WithValue(ctx, extensions.ContextKeyIsProvider, "app") + return context.WithValue(ctx, extensions.ContextKeyIsChannel, addr) +} + +// Close will clean up any existing resources on the controller +func (c *AppController) Close(ctx context.Context) { + // Unsubscribing remaining channels +} + +// UserController is the structure that provides sending capabilities to the +// developer and and connect the broker with the User +type UserController struct { + controller +} + +// NewUserController links the User to the broker +func NewUserController(bc extensions.BrokerController, options ...ControllerOption) (*UserController, error) { + // Check if broker controller has been provided + if bc == nil { + return nil, extensions.ErrNilBrokerController + } + + // Create default controller + controller := controller{ + broker: bc, + subscriptions: make(map[string]extensions.BrokerChannelSubscription), + logger: extensions.DummyLogger{}, + middlewares: make([]extensions.Middleware, 0), + errorHandler: extensions.DefaultErrorHandler(), + } + + // Apply options + for _, option := range options { + option(&controller) + } + + return &UserController{controller: controller}, nil +} + +func (c UserController) wrapMiddlewares( + middlewares []extensions.Middleware, + callback extensions.NextMiddleware, +) func(ctx context.Context, msg *extensions.BrokerMessage) error { + var called bool + + // If there is no more middleware + if len(middlewares) == 0 { + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the callback if it exists and it has not been called already + if callback != nil && !called { + called = true + return callback(ctx) + } + + // Nil can be returned, as the callback has already been called + return nil + } + } + + // Get the next function to call from next middlewares or callback + next := c.wrapMiddlewares(middlewares[1:], callback) + + // Wrap middleware into a check function that will call execute the middleware + // and call the next wrapped middleware if the returned function has not been + // called already + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the middleware and the following if it has not been done already + if !called { + // Create the next call with the context and the message + nextWithArgs := func(ctx context.Context) error { + return next(ctx, msg) + } + + // Call the middleware and register it as already called + called = true + if err := middlewares[0](ctx, msg, nextWithArgs); err != nil { + return err + } + + // If next has already been called in middleware, it should not be executed again + return nextWithArgs(ctx) + } + + // Nil can be returned, as the next middleware has already been called + return nil + } +} + +func (c UserController) executeMiddlewares(ctx context.Context, msg *extensions.BrokerMessage, callback extensions.NextMiddleware) error { + // Wrap middleware to have 'next' function when calling them + wrapped := c.wrapMiddlewares(c.middlewares, callback) + + // Execute wrapped middlewares + return wrapped(ctx, msg) +} + +func addUserContextValues(ctx context.Context, addr string) context.Context { + ctx = context.WithValue(ctx, extensions.ContextKeyIsVersion, "1.2.3") + ctx = context.WithValue(ctx, extensions.ContextKeyIsProvider, "user") + return context.WithValue(ctx, extensions.ContextKeyIsChannel, addr) +} + +// Close will clean up any existing resources on the controller +func (c *UserController) Close(ctx context.Context) { + // Unsubscribing remaining channels +} + +// AsyncAPIVersion is the version of the used AsyncAPI document +const AsyncAPIVersion = "1.2.3" + +// controller is the controller that will be used to communicate with the broker +// It will be used internally by AppController and UserController +type controller struct { + // broker is the broker controller that will be used to communicate + broker extensions.BrokerController + // subscriptions is a map of all subscriptions + subscriptions map[string]extensions.BrokerChannelSubscription + // logger is the logger that will be used² to log operations on controller + logger extensions.Logger + // middlewares are the middlewares that will be executed when sending or + // receiving messages + middlewares []extensions.Middleware + // handler to handle errors from consumers and middlewares + errorHandler extensions.ErrorHandler +} + +// ControllerOption is the type of the options that can be passed +// when creating a new Controller +type ControllerOption func(controller *controller) + +// WithLogger attaches a logger to the controller +func WithLogger(logger extensions.Logger) ControllerOption { + return func(controller *controller) { + controller.logger = logger + } +} + +// WithMiddlewares attaches middlewares that will be executed when sending or receiving messages +func WithMiddlewares(middlewares ...extensions.Middleware) ControllerOption { + return func(controller *controller) { + controller.middlewares = middlewares + } +} + +// WithErrorHandler attaches a errorhandler to handle errors from subscriber functions +func WithErrorHandler(handler extensions.ErrorHandler) ControllerOption { + return func(controller *controller) { + controller.errorHandler = handler + } +} + +type MessageWithCorrelationID interface { + CorrelationID() string + SetCorrelationID(id string) +} + +type Error struct { + Channel string + Err error +} + +func (e *Error) Error() string { + return fmt.Sprintf("channel %q: err %v", e.Channel, e.Err) +} + +// TestMessagePayload is a schema from the AsyncAPI specification required in messages +type TestMessagePayload struct { + NonReqArray []string `json:"nonReqArray,omitempty"` + NonReqField *string `json:"nonReqField,omitempty"` + ReqArray []string `json:"reqArray" validate:"required"` + ReqField string `json:"reqField"` +} + +// TestMessage is the message expected for 'TestMessage' channel. +type TestMessage struct { + // Payload will be inserted in the message payload + Payload TestMessagePayload +} + +func NewTestMessage() TestMessage { + var msg TestMessage + + return msg +} + +// brokerMessageToTestMessage will fill a new TestMessage with data from generic broker message +func brokerMessageToTestMessage(bMsg extensions.BrokerMessage) (TestMessage, error) { + var msg TestMessage + + // Unmarshal payload to expected message payload format + err := json.Unmarshal(bMsg.Payload, &msg.Payload) + if err != nil { + return msg, err + } + + // TODO: run checks on msg type + + return msg, nil +} + +// toBrokerMessage will generate a generic broker message from TestMessage data +func (msg TestMessage) toBrokerMessage() (extensions.BrokerMessage, error) { + // TODO: implement checks on message + + // Marshal payload to JSON + payload, err := json.Marshal(msg.Payload) + if err != nil { + return extensions.BrokerMessage{}, err + } + + // There is no headers here + headers := make(map[string][]byte, 0) + + return extensions.BrokerMessage{ + Headers: headers, + Payload: payload, + }, nil +} diff --git a/test/v3/issues/259/forcepointers/asyncapi.gen.go b/test/v3/issues/259/forcepointers/asyncapi.gen.go new file mode 100644 index 0000000..dbaa0c3 --- /dev/null +++ b/test/v3/issues/259/forcepointers/asyncapi.gen.go @@ -0,0 +1,320 @@ +// Package "issue259forcepointers" provides primitives to interact with the AsyncAPI specification. +// +// Code generated by github.com/lerenn/asyncapi-codegen version (devel) DO NOT EDIT. +package issue259forcepointers + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/lerenn/asyncapi-codegen/pkg/extensions" +) + +// AppController is the structure that provides sending capabilities to the +// developer and and connect the broker with the App +type AppController struct { + controller +} + +// NewAppController links the App to the broker +func NewAppController(bc extensions.BrokerController, options ...ControllerOption) (*AppController, error) { + // Check if broker controller has been provided + if bc == nil { + return nil, extensions.ErrNilBrokerController + } + + // Create default controller + controller := controller{ + broker: bc, + subscriptions: make(map[string]extensions.BrokerChannelSubscription), + logger: extensions.DummyLogger{}, + middlewares: make([]extensions.Middleware, 0), + errorHandler: extensions.DefaultErrorHandler(), + } + + // Apply options + for _, option := range options { + option(&controller) + } + + return &AppController{controller: controller}, nil +} + +func (c AppController) wrapMiddlewares( + middlewares []extensions.Middleware, + callback extensions.NextMiddleware, +) func(ctx context.Context, msg *extensions.BrokerMessage) error { + var called bool + + // If there is no more middleware + if len(middlewares) == 0 { + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the callback if it exists and it has not been called already + if callback != nil && !called { + called = true + return callback(ctx) + } + + // Nil can be returned, as the callback has already been called + return nil + } + } + + // Get the next function to call from next middlewares or callback + next := c.wrapMiddlewares(middlewares[1:], callback) + + // Wrap middleware into a check function that will call execute the middleware + // and call the next wrapped middleware if the returned function has not been + // called already + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the middleware and the following if it has not been done already + if !called { + // Create the next call with the context and the message + nextWithArgs := func(ctx context.Context) error { + return next(ctx, msg) + } + + // Call the middleware and register it as already called + called = true + if err := middlewares[0](ctx, msg, nextWithArgs); err != nil { + return err + } + + // If next has already been called in middleware, it should not be executed again + return nextWithArgs(ctx) + } + + // Nil can be returned, as the next middleware has already been called + return nil + } +} + +func (c AppController) executeMiddlewares(ctx context.Context, msg *extensions.BrokerMessage, callback extensions.NextMiddleware) error { + // Wrap middleware to have 'next' function when calling them + wrapped := c.wrapMiddlewares(c.middlewares, callback) + + // Execute wrapped middlewares + return wrapped(ctx, msg) +} + +func addAppContextValues(ctx context.Context, addr string) context.Context { + ctx = context.WithValue(ctx, extensions.ContextKeyIsVersion, "1.2.3") + ctx = context.WithValue(ctx, extensions.ContextKeyIsProvider, "app") + return context.WithValue(ctx, extensions.ContextKeyIsChannel, addr) +} + +// Close will clean up any existing resources on the controller +func (c *AppController) Close(ctx context.Context) { + // Unsubscribing remaining channels +} + +// UserController is the structure that provides sending capabilities to the +// developer and and connect the broker with the User +type UserController struct { + controller +} + +// NewUserController links the User to the broker +func NewUserController(bc extensions.BrokerController, options ...ControllerOption) (*UserController, error) { + // Check if broker controller has been provided + if bc == nil { + return nil, extensions.ErrNilBrokerController + } + + // Create default controller + controller := controller{ + broker: bc, + subscriptions: make(map[string]extensions.BrokerChannelSubscription), + logger: extensions.DummyLogger{}, + middlewares: make([]extensions.Middleware, 0), + errorHandler: extensions.DefaultErrorHandler(), + } + + // Apply options + for _, option := range options { + option(&controller) + } + + return &UserController{controller: controller}, nil +} + +func (c UserController) wrapMiddlewares( + middlewares []extensions.Middleware, + callback extensions.NextMiddleware, +) func(ctx context.Context, msg *extensions.BrokerMessage) error { + var called bool + + // If there is no more middleware + if len(middlewares) == 0 { + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the callback if it exists and it has not been called already + if callback != nil && !called { + called = true + return callback(ctx) + } + + // Nil can be returned, as the callback has already been called + return nil + } + } + + // Get the next function to call from next middlewares or callback + next := c.wrapMiddlewares(middlewares[1:], callback) + + // Wrap middleware into a check function that will call execute the middleware + // and call the next wrapped middleware if the returned function has not been + // called already + return func(ctx context.Context, msg *extensions.BrokerMessage) error { + // Call the middleware and the following if it has not been done already + if !called { + // Create the next call with the context and the message + nextWithArgs := func(ctx context.Context) error { + return next(ctx, msg) + } + + // Call the middleware and register it as already called + called = true + if err := middlewares[0](ctx, msg, nextWithArgs); err != nil { + return err + } + + // If next has already been called in middleware, it should not be executed again + return nextWithArgs(ctx) + } + + // Nil can be returned, as the next middleware has already been called + return nil + } +} + +func (c UserController) executeMiddlewares(ctx context.Context, msg *extensions.BrokerMessage, callback extensions.NextMiddleware) error { + // Wrap middleware to have 'next' function when calling them + wrapped := c.wrapMiddlewares(c.middlewares, callback) + + // Execute wrapped middlewares + return wrapped(ctx, msg) +} + +func addUserContextValues(ctx context.Context, addr string) context.Context { + ctx = context.WithValue(ctx, extensions.ContextKeyIsVersion, "1.2.3") + ctx = context.WithValue(ctx, extensions.ContextKeyIsProvider, "user") + return context.WithValue(ctx, extensions.ContextKeyIsChannel, addr) +} + +// Close will clean up any existing resources on the controller +func (c *UserController) Close(ctx context.Context) { + // Unsubscribing remaining channels +} + +// AsyncAPIVersion is the version of the used AsyncAPI document +const AsyncAPIVersion = "1.2.3" + +// controller is the controller that will be used to communicate with the broker +// It will be used internally by AppController and UserController +type controller struct { + // broker is the broker controller that will be used to communicate + broker extensions.BrokerController + // subscriptions is a map of all subscriptions + subscriptions map[string]extensions.BrokerChannelSubscription + // logger is the logger that will be used² to log operations on controller + logger extensions.Logger + // middlewares are the middlewares that will be executed when sending or + // receiving messages + middlewares []extensions.Middleware + // handler to handle errors from consumers and middlewares + errorHandler extensions.ErrorHandler +} + +// ControllerOption is the type of the options that can be passed +// when creating a new Controller +type ControllerOption func(controller *controller) + +// WithLogger attaches a logger to the controller +func WithLogger(logger extensions.Logger) ControllerOption { + return func(controller *controller) { + controller.logger = logger + } +} + +// WithMiddlewares attaches middlewares that will be executed when sending or receiving messages +func WithMiddlewares(middlewares ...extensions.Middleware) ControllerOption { + return func(controller *controller) { + controller.middlewares = middlewares + } +} + +// WithErrorHandler attaches a errorhandler to handle errors from subscriber functions +func WithErrorHandler(handler extensions.ErrorHandler) ControllerOption { + return func(controller *controller) { + controller.errorHandler = handler + } +} + +type MessageWithCorrelationID interface { + CorrelationID() string + SetCorrelationID(id string) +} + +type Error struct { + Channel string + Err error +} + +func (e *Error) Error() string { + return fmt.Sprintf("channel %q: err %v", e.Channel, e.Err) +} + +// TestMessagePayload is a schema from the AsyncAPI specification required in messages +type TestMessagePayload struct { + NonReqArray []string `json:"nonReqArray,omitempty"` + NonReqField *string `json:"nonReqField,omitempty"` + ReqArray []string `json:"reqArray" validate:"required"` + ReqField *string `json:"reqField" validate:"required"` +} + +// TestMessage is the message expected for 'TestMessage' channel. +type TestMessage struct { + // Payload will be inserted in the message payload + Payload TestMessagePayload +} + +func NewTestMessage() TestMessage { + var msg TestMessage + + return msg +} + +// brokerMessageToTestMessage will fill a new TestMessage with data from generic broker message +func brokerMessageToTestMessage(bMsg extensions.BrokerMessage) (TestMessage, error) { + var msg TestMessage + + // Unmarshal payload to expected message payload format + err := json.Unmarshal(bMsg.Payload, &msg.Payload) + if err != nil { + return msg, err + } + + // TODO: run checks on msg type + + return msg, nil +} + +// toBrokerMessage will generate a generic broker message from TestMessage data +func (msg TestMessage) toBrokerMessage() (extensions.BrokerMessage, error) { + // TODO: implement checks on message + + // Marshal payload to JSON + payload, err := json.Marshal(msg.Payload) + if err != nil { + return extensions.BrokerMessage{}, err + } + + // There is no headers here + headers := make(map[string][]byte, 0) + + return extensions.BrokerMessage{ + Headers: headers, + Payload: payload, + }, nil +} diff --git a/test/v3/issues/259/suite_test.go b/test/v3/issues/259/suite_test.go new file mode 100644 index 0000000..e5f282e --- /dev/null +++ b/test/v3/issues/259/suite_test.go @@ -0,0 +1,107 @@ +//go:generate go run ../../../../cmd/asyncapi-codegen -p issue259default -i ./asyncapi.yaml -o ./default/asyncapi.gen.go +//go:generate go run ../../../../cmd/asyncapi-codegen -p issue259forcepointers --force-pointers -i ./asyncapi.yaml -o ./forcepointers/asyncapi.gen.go + +package issue259 + +import ( + "encoding/json" + "testing" + + "github.com/go-playground/validator/v10" + issue259default "github.com/lerenn/asyncapi-codegen/test/v3/issues/259/default" + issue259forcepointers "github.com/lerenn/asyncapi-codegen/test/v3/issues/259/forcepointers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +func TestSuite(t *testing.T) { + suite.Run(t, NewSuite()) +} + +type Suite struct { + suite.Suite +} + +func NewSuite() *Suite { + return &Suite{} +} + +func (suite *Suite) TestDefault() { + validate := validator.New() + + // Required absent - no error due to empty/nil fields + assert.NoError(suite.T(), validate.Struct( + issue259default.TestMessagePayload{ + NonReqArray: nil, + NonReqField: nil, + ReqArray: []string{}, + ReqField: "", + }, + )) + + var out issue259default.TestMessagePayload + err := json.Unmarshal([]byte(`{}`), &out) + assert.NoError(suite.T(), err) +} + +func (suite *Suite) TestForcePointers() { + validate := validator.New() + + cases := []struct { + name string + inJSON string + + isErrExpected bool + }{ + { + name: "empty input", + inJSON: `{}`, + isErrExpected: true, + }, + { + name: "full input", + inJSON: `{"reqField": "something","nonReqField": "something","reqArray": ["something"], +"nonReqArray": ["something"]}`, + isErrExpected: false, + }, + { + name: "only required fields", + inJSON: `{"reqField": "something","reqArray": ["something"]}`, + isErrExpected: false, + }, + { + name: "empty array", + inJSON: `{"reqField": "something","reqArray": []}`, + isErrExpected: false, + }, + { + name: "empty string", + inJSON: `{"reqField": "","reqArray": ["something"]}`, + isErrExpected: false, + }, + { + name: "missing string", + inJSON: `{"reqArray": ["something"]}`, + isErrExpected: true, + }, + { + name: "missing array", + inJSON: `{"reqField": "something"}`, + isErrExpected: true, + }, + } + + for _, tc := range cases { + suite.T().Run(tc.name, func(t *testing.T) { + var out issue259forcepointers.TestMessagePayload + err := json.Unmarshal([]byte(tc.inJSON), &out) + assert.NoError(t, err) + + if tc.isErrExpected { + assert.Error(t, validate.Struct(out)) + } else { + assert.NoError(t, validate.Struct(out)) + } + }) + } +}