Skip to content
Closed
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
102 changes: 102 additions & 0 deletions pkg/codegen/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,108 @@ func TestGoTypeImport(t *testing.T) {
}
}

func TestRequiredBody(t *testing.T) {
packageName := "api"
opts := Configuration{
PackageName: packageName,
Generate: GenerateOptions{
Models: true,
StdHTTPServer: true,
Strict: true,
},
}
spec := "test_specs/required-body.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)

assert.Contains(t, code, `type PostRequestsRequestObject struct {
Body *PostRequestsJSONRequestBody
}`)

assert.Contains(t, code, `type PostRequiredrequestsRequestObject struct {
Body *PostRequiredrequestsJSONRequestBody
}`)

assert.Contains(t, code, `var request PostRequestsRequestObject

var body PostRequestsJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
return
}
request.Body = &body`)
assert.Contains(t, code, `var request PostRequiredrequestsRequestObject

var body PostRequiredrequestsJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
return
}
request.Body = &body`)
}

func TestRequiredBodyWithOption(t *testing.T) {
packageName := "api"
opts := Configuration{
PackageName: packageName,
Generate: GenerateOptions{
Models: true,
StdHTTPServer: true,
Strict: true,
},
OutputOptions: OutputOptions{RequiredRequestBody: true},
}
spec := "test_specs/required-body.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)

assert.Contains(t, code, `type PostRequestsRequestObject struct {
JSONBody *PostRequestsJSONRequestBody
}`)

assert.Contains(t, code, `type PostRequiredrequestsRequestObject struct {
Body PostRequiredrequestsJSONRequestBody
}`)

assert.Contains(t, code, `var request PostRequestsRequestObject

if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {

var body PostRequestsJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
return
}
request.JSONBody = &body
}`)
assert.Contains(t, code, `var request PostRequiredrequestsRequestObject

var body PostRequiredrequestsJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
return
}
request.Body = body`)
}

func TestRemoteExternalReference(t *testing.T) {
if testing.Short() {
t.Skip("Skipping test that interacts with the network")
Expand Down
2 changes: 2 additions & 0 deletions pkg/codegen/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ type OutputOptions struct {
InitialismOverrides bool `yaml:"initialism-overrides,omitempty"`
// Whether to generate nullable type for nullable fields
NullableType bool `yaml:"nullable-type,omitempty"`
// RequiredRequestBody do stuff
RequiredRequestBody bool `yaml:"required-request-body,omitempty"`

// DisableTypeAliasesForType allows defining which OpenAPI `type`s will explicitly not use type aliases
// Currently supports:
Expand Down
28 changes: 28 additions & 0 deletions pkg/codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,23 @@ func (o OperationDefinition) HasMaskedRequestContentTypes() bool {
return false
}

func (o OperationDefinition) NeedsContentTypeCheck() bool {
if len(o.Bodies) > 1 {
return true
}
// if we don't want to handle request bodies being required or not, we skip the rest of this function.
if !globalState.options.OutputOptions.RequiredRequestBody {
return false
}
for _, body := range o.Bodies {
if !body.Required {
return true
}
}

return false
}

// RequestBodyDefinition describes a request body
type RequestBodyDefinition struct {
// Is this body required, or optional?
Expand Down Expand Up @@ -410,6 +427,17 @@ func (r RequestBodyDefinition) Suffix() string {
return "With" + r.NameTag + "Body"
}

// IsRequired returns if a request body is required
// if we don't want to handle request bodies being required or not, we mark them all as not required
// otherwise it is the value set in the open api specification
func (r RequestBodyDefinition) IsRequired() bool {
if !globalState.options.OutputOptions.RequiredRequestBody {
return false
}

return r.Required
}

// IsSupportedByClient returns true if we support this content type for client. Otherwise only generic method will ge generated
func (r RequestBodyDefinition) IsSupportedByClient() bool {
return r.IsJSON() || r.NameTag == "Formdata" || r.NameTag == "Text"
Expand Down
18 changes: 9 additions & 9 deletions pkg/codegen/templates/strict/strict-http.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ type strictHandler struct {
request.ContentType = r.Header.Get("Content-Type")
{{end -}}

{{$multipleBodies := gt (len .Bodies) 1 -}}
{{$needsContentTypeCheck := .NeedsContentTypeCheck -}}
{{range .Bodies -}}
{{if $multipleBodies}}if strings.HasPrefix(r.Header.Get("Content-Type"), "{{.ContentType}}") { {{end}}
{{if $needsContentTypeCheck}}if strings.HasPrefix(r.Header.Get("Content-Type"), "{{.ContentType}}") { {{end}}
{{if .IsJSON }}
var body {{$opid}}{{.NameTag}}RequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
return
}
request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body
request.{{if $needsContentTypeCheck}}{{.NameTag}}{{end}}Body = {{if not .IsRequired}}&{{end}}body
{{else if eq .NameTag "Formdata" -}}
if err := r.ParseForm(); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode formdata: %w", err))
Expand All @@ -65,14 +65,14 @@ type strictHandler struct {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't bind formdata: %w", err))
return
}
request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body
request.{{if $needsContentTypeCheck}}{{.NameTag}}{{end}}Body = {{if not .IsRequired}}&{{end}}body
{{else if eq .NameTag "Multipart" -}}
{{if eq .ContentType "multipart/form-data" -}}
if reader, err := r.MultipartReader(); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode multipart body: %w", err))
return
} else {
request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = reader
request.{{if $needsContentTypeCheck}}{{.NameTag}}{{end}}Body = reader
}
{{else -}}
if _, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")); err != nil {
Expand All @@ -82,7 +82,7 @@ type strictHandler struct {
sh.options.RequestErrorHandlerFunc(w, r, http.ErrMissingBoundary)
return
} else {
request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = multipart.NewReader(r.Body, boundary)
request.{{if $needsContentTypeCheck}}{{.NameTag}}{{end}}Body = multipart.NewReader(r.Body, boundary)
}
{{end -}}
{{else if eq .NameTag "Text" -}}
Expand All @@ -92,11 +92,11 @@ type strictHandler struct {
return
}
body := {{$opid}}{{.NameTag}}RequestBody(data)
request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = &body
request.{{if $needsContentTypeCheck}}{{.NameTag}}{{end}}Body = {{if not .IsRequired}}&{{end}}body
{{else -}}
request.{{if $multipleBodies}}{{.NameTag}}{{end}}Body = r.Body
request.{{if $needsContentTypeCheck}}{{.NameTag}}{{end}}Body = r.Body
{{end}}{{/* if eq .NameTag "JSON" */ -}}
{{if $multipleBodies}}}{{end}}
{{if $needsContentTypeCheck}}}{{end}}
{{end}}{{/* range .Bodies */}}

handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/codegen/templates/strict/strict-interface.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
{{if .HasMaskedRequestContentTypes -}}
ContentType string
{{end -}}
{{$multipleBodies := gt (len .Bodies) 1 -}}
{{$needsContentTypeCheck := .NeedsContentTypeCheck -}}
{{range .Bodies -}}
{{if $multipleBodies}}{{.NameTag}}{{end}}Body {{if eq .NameTag "Multipart"}}*multipart.Reader{{else if ne .NameTag ""}}*{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}}
{{if $needsContentTypeCheck}}{{.NameTag}}{{end}}Body {{if eq .NameTag "Multipart"}}{{if not .IsRequired}}*{{end}}multipart.Reader{{else if ne .NameTag ""}}{{if not .IsRequired}}*{{end}}{{$opid}}{{.NameTag}}RequestBody{{else}}io.Reader{{end}}
{{end -}}
}

Expand Down
61 changes: 61 additions & 0 deletions pkg/codegen/test_specs/required-body.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification
termsOfService: http://swagger.io/terms/
contact:
name: Swagger API Team
email: apiteam@swagger.io
url: http://swagger.io
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: http://petstore.swagger.io/api
paths:
/requiredrequests/:
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
one:
type: string
required:
- one
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
two:
type: string
/requests/:
post:
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
one:
type: string
required:
- one
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
two:
type: string