Skip to content
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,20 @@ which help you to use the various OpenAPI 3 Authentication mechanism.
will override any default value. This extended property isn't supported in all parts of
OpenAPI, so please refer to the spec as to where it's allowed. Swagger validation tools will
flag incorrect usage of this property.
- `x-go-type-skip-optional-pointer`: specifies if the Go type should or should not be a pointer
when the property is optional. If set to true, the type will not be a pointer if the field is
optional or nullable. If set to false, the type will be a pointer.

```yaml
properties:
field:
type: string
x-go-type-skip-optional-pointer: true
```

In the example above, the `field` field will be of type `string` instead of `*string`. This is
useful when you want to handle the case of an empty string differently than a null value.

- `x-go-name`: specifies Go field name. It allows you to specify the field name for a schema, and
will override any default value. This extended property isn't supported in all parts of
OpenAPI, so please refer to the spec as to where it's allowed. Swagger validation tools will
Expand Down
47 changes: 43 additions & 4 deletions pkg/codegen/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import (
"net/http"
"testing"

examplePetstoreClient "github.com/deepmap/oapi-codegen/examples/petstore-expanded"
examplePetstore "github.com/deepmap/oapi-codegen/examples/petstore-expanded/echo/api"
"github.com/deepmap/oapi-codegen/pkg/util"
"github.com/getkin/kin-openapi/openapi3"
"github.com/golangci/lint-1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

examplePetstoreClient "github.com/deepmap/oapi-codegen/examples/petstore-expanded"
examplePetstore "github.com/deepmap/oapi-codegen/examples/petstore-expanded/echo/api"
"github.com/deepmap/oapi-codegen/pkg/util"
)

const (
Expand Down Expand Up @@ -156,7 +157,7 @@ func TestExamplePetStoreCodeGenerationWithHTTPUserTemplates(t *testing.T) {
defer ln.Close()

//nolint:errcheck
//Does not matter if the server returns an error on close etc.
// Does not matter if the server returns an error on close etc.
go http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, writeErr := w.Write([]byte("//blah"))
assert.NoError(t, writeErr)
Expand Down Expand Up @@ -288,6 +289,44 @@ type GetTestByNameResponse struct {
checkLint(t, "test.gen.go", []byte(code))
}

func TestExtPropGoTypeSkipOptionalPointer(t *testing.T) {
packageName := "api"
opts := Configuration{
PackageName: packageName,
Generate: GenerateOptions{
EchoServer: true,
Models: true,
EmbeddedSpec: true,
Strict: true,
},
}
spec := "test_specs/x-go-type-skip-optional-pointer.yaml"
swagger, err := util.LoadSwagger(spec)
require.NoError(t, err)

// Run our code generation:
code, err := Generate(swagger, opts)
assert.NoError(t, err)
assert.NotEmpty(t, code)

// Check that we have valid (formattable) code:
_, err = format.Source([]byte(code))
assert.NoError(t, err)

// Check that optional pointer fields are skipped if requested
assert.Contains(t, code, "NullableFieldSkipFalse *string `json:\"nullableFieldSkipFalse\"`")
assert.Contains(t, code, "NullableFieldSkipTrue string `json:\"nullableFieldSkipTrue\"`")
assert.Contains(t, code, "OptionalField *string `json:\"optionalField,omitempty\"`")
assert.Contains(t, code, "OptionalFieldSkipFalse *string `json:\"optionalFieldSkipFalse,omitempty\"`")
assert.Contains(t, code, "OptionalFieldSkipTrue string `json:\"optionalFieldSkipTrue,omitempty\"`")

// Check that the extension applies on custom types as well
assert.Contains(t, code, "CustomTypeWithSkipTrue string `json:\"customTypeWithSkipTrue,omitempty\"`")

// Check that the extension has no effect on required fields
assert.Contains(t, code, "RequiredField string `json:\"requiredField\"`")
}

func TestGoTypeImport(t *testing.T) {
packageName := "api"
opts := Configuration{
Expand Down
12 changes: 12 additions & 0 deletions pkg/codegen/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
const (
// extPropGoType overrides the generated type definition.
extPropGoType = "x-go-type"
// extPropGoTypeSkipOptionalPointer specifies that optional fields should
// be the type itself instead of a pointer to the type.
extPropGoTypeSkipOptionalPointer = "x-go-type-skip-optional-pointer"
// extPropGoImport specifies the module to import which provides above type
extPropGoImport = "x-go-type-import"
// extGoName is used to override a field name
Expand All @@ -28,10 +31,19 @@ func extString(extPropValue interface{}) (string, error) {
}
return str, nil
}

func extTypeName(extPropValue interface{}) (string, error) {
return extString(extPropValue)
}

func extParsePropGoTypeSkipOptionalPointer(extPropValue interface{}) (bool, error) {
goTypeSkipOptionalPointer, ok := extPropValue.(bool)
if !ok {
return false, fmt.Errorf("failed to convert type: %T", extPropValue)
}
return goTypeSkipOptionalPointer, nil
}

func extParseGoFieldName(extPropValue interface{}) (string, error) {
return extString(extPropValue)
}
Expand Down
54 changes: 54 additions & 0 deletions pkg/codegen/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,57 @@ func Test_extTypeName(t *testing.T) {
})
}
}

func Test_extParsePropGoTypeSkipOptionalPointer(t *testing.T) {
type args struct {
extPropValue json.RawMessage
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
{
name: "success when set to true",
args: args{json.RawMessage(`true`)},
want: true,
wantErr: false,
},
{
name: "success when set to false",
args: args{json.RawMessage(`false`)},
want: false,
wantErr: false,
},
{
name: "nil conversion error",
args: args{nil},
want: false,
wantErr: true,
},
{
name: "type conversion error",
args: args{json.RawMessage(`"true"`)},
want: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// kin-openapi no longer returns these as RawMessage
var extPropValue interface{}
if tt.args.extPropValue != nil {
err := json.Unmarshal(tt.args.extPropValue, &extPropValue)
assert.NoError(t, err)
}
got, err := extParsePropGoTypeSkipOptionalPointer(extPropValue)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
18 changes: 18 additions & 0 deletions pkg/codegen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,16 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {
return outSchema, nil
}

// Check x-go-type-skip-optional-pointer, which will override if the type
// should be a pointer or not when the field is optional.
if extension, ok := schema.Extensions[extPropGoTypeSkipOptionalPointer]; ok {
skipOptionalPointer, err := extParsePropGoTypeSkipOptionalPointer(extension)
if err != nil {
return outSchema, fmt.Errorf("invalid value for %q: %w", extPropGoTypeSkipOptionalPointer, err)
}
outSchema.SkipOptionalPointer = skipOptionalPointer
}

// Schema type and format, eg. string / binary
t := schema.Type
// Handle objects and empty schemas first as a special case
Expand Down Expand Up @@ -661,6 +671,14 @@ func GenFieldsFromProperties(props []Property) []string {
field += fmt.Sprintf("%s\n", DeprecationComment(deprecationReason))
}

// Check x-go-type-skip-optional-pointer, which will override if the type
// should be a pointer or not when the field is optional.
if extension, ok := p.Extensions[extPropGoTypeSkipOptionalPointer]; ok {
if skipOptionalPointer, err := extParsePropGoTypeSkipOptionalPointer(extension); err == nil {
p.Schema.SkipOptionalPointer = skipOptionalPointer
}
}

field += fmt.Sprintf(" %s %s", goFieldName, p.GoTypeDef())

omitEmpty := !p.Nullable &&
Expand Down
54 changes: 54 additions & 0 deletions pkg/codegen/test_specs/x-go-type-skip-optional-pointer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Example with x-go-type-skip-optional-pointer
paths:
/check:
get:
summary: Return example
responses:
'200':
description: Ok
content:
application/json:
schema:
required:
- requiredField
properties:
# Optional field where the type is a pointer to a string, and
# the x-go-type-skip-optional-pointer is not set.
optionalField:
type: string
# Optional field where the type is a pointer to a string, and
# the x-go-type-skip-optional-pointer is set to false.
optionalFieldSkipFalse:
type: string
x-go-type-skip-optional-pointer: false
# Optional field where the type is a pointer to a string, and
# the x-go-type-skip-optional-pointer is set to true.
optionalFieldSkipTrue:
type: string
x-go-type-skip-optional-pointer: true
# Optional field where the type is a pointer to a string, and
# the x-go-type-skip-optional-pointer is set to true.
customTypeWithSkipTrue:
type: string
x-go-type: string
x-go-type-skip-optional-pointer: true
# Nullable field where the type is a pointer to a string, and
# the x-go-type-skip-optional-pointer is set to false.
nullableFieldSkipFalse:
type: string
nullable: true
x-go-type-skip-optional-pointer: false
# Nullable field where the type is a pointer to a string, and
# the x-go-type-skip-optional-pointer is set to true.
nullableFieldSkipTrue:
type: string
nullable: true
x-go-type-skip-optional-pointer: true
# Check that x-go-type-skip-optional-pointer has no effect on
# required fields.
requiredField:
type: string
x-go-type-skip-optional-pointer: false