Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(codegen): Add a flag to force field generation as pointers to make validations work #260

Merged
merged 1 commit into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!
Expand Down
5 changes: 5 additions & 0 deletions cmd/asyncapi-codegen/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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 != "" {
Expand Down
4 changes: 2 additions & 2 deletions examples/ping/v2/kafka/app/app.gen.go

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

4 changes: 2 additions & 2 deletions examples/ping/v2/kafka/user/user.gen.go

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

4 changes: 2 additions & 2 deletions examples/ping/v2/nats-jetstream/app/app.gen.go

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

4 changes: 2 additions & 2 deletions examples/ping/v2/nats-jetstream/user/user.gen.go

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

4 changes: 2 additions & 2 deletions examples/ping/v2/nats/app/app.gen.go

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

4 changes: 2 additions & 2 deletions examples/ping/v2/nats/user/user.gen.go

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

6 changes: 6 additions & 0 deletions pkg/codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions pkg/codegen/generators/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
12 changes: 12 additions & 0 deletions pkg/codegen/generators/v2/templates/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,25 @@ 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 {
return template.FuncMap{
"getChildrenObjectSchemas": GetChildrenObjectSchemas,
"channelToMessage": ChannelToMessage,
"isRequired": IsRequired,
"isFieldPointer": isFieldPointer,
"generateChannelPath": GenerateChannelPath,
"referenceToStructAttributePath": ReferenceToStructAttributePath,
"operationName": OperationName,
Expand Down
2 changes: 1 addition & 1 deletion pkg/codegen/generators/v2/templates/schema_definition.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
12 changes: 12 additions & 0 deletions pkg/codegen/generators/v3/templates/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -136,6 +147,7 @@ func HelpersFunctions() template.FuncMap {
"opToMsgTypeName": OpToMsgTypeName,
"opToChannelTypeName": OpToChannelTypeName,
"isRequired": IsRequired,
"isFieldPointer": isFieldPointer,
"generateChannelAddr": GenerateChannelAddr,
"generateChannelAddrFromOp": GenerateChannelAddrFromOp,
"referenceToStructAttributePath": ReferenceToStructAttributePath,
Expand Down
2 changes: 1 addition & 1 deletion pkg/codegen/generators/v3/templates/schema_definition.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
3 changes: 3 additions & 0 deletions pkg/codegen/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion test/v2/issues/131/asyncapi.gen.go

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

7 changes: 0 additions & 7 deletions test/v2/issues/131/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
12 changes: 6 additions & 6 deletions test/v2/issues/185/asyncapi.gen.go

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

2 changes: 1 addition & 1 deletion test/v2/issues/245/asyncapi.gen.go

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

27 changes: 27 additions & 0 deletions test/v2/issues/259/asyncapi.yaml
Original file line number Diff line number Diff line change
@@ -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

Loading