Skip to content

Commit

Permalink
Fix handling of request input that can accept form or json
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop committed Nov 7, 2024
1 parent 3e4a609 commit 41ffc11
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 0 deletions.
35 changes: 35 additions & 0 deletions _examples/advanced-generic-openapi31/form_or_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

Check notice on line 1 in _examples/advanced-generic-openapi31/form_or_json.go

View workflow job for this annotation

GitHub Actions / test (1.22.x)

File is not covered by tests.

import (
"context"

"github.com/swaggest/usecase"
)

type formOrJSONInput struct {
Field1 string `json:"field1" formData:"field1" required:"true"`
Field2 int `json:"field2" formData:"field2" required:"true"`
Field3 string `path:"path" required:"true"`
}

func (formOrJSONInput) ForceJSONRequestBody() {}

func formOrJSON() usecase.Interactor {
type formOrJSONOutput struct {
F1 string `json:"f1"`
F2 int `json:"f2"`
F3 string `json:"f3"`
}

u := usecase.NewInteractor(func(ctx context.Context, input formOrJSONInput, output *formOrJSONOutput) error {
output.F1 = input.Field1
output.F2 = input.Field2
output.F3 = input.Field3

return nil
})

u.SetTags("Request")

return u
}
39 changes: 39 additions & 0 deletions _examples/advanced-generic-openapi31/form_or_json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/bool64/httptestbench"
"github.com/valyala/fasthttp"
)

func Benchmark_formOrJSON(b *testing.B) {
r := NewRouter()

srv := httptest.NewServer(r)
defer srv.Close()

b.Run("form", func(b *testing.B) {
httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) {
req.Header.SetMethod(http.MethodPost)
req.SetRequestURI(srv.URL + "/form-or-json/abc")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBody([]byte(`field1=def&field2=123`))
}, func(i int, resp *fasthttp.Response) bool {
return resp.StatusCode() == http.StatusOK
})
})

b.Run("json", func(b *testing.B) {
httptestbench.RoundTrip(b, 50, func(i int, req *fasthttp.Request) {
req.Header.SetMethod(http.MethodPost)
req.SetRequestURI(srv.URL + "/form-or-json/abc")
req.Header.Set("Content-Type", "application/json")
req.SetBody([]byte(`{"field1":"string","field2":0}`))
}, func(i int, resp *fasthttp.Response) bool {
return resp.StatusCode() == http.StatusOK
})
})
}
2 changes: 2 additions & 0 deletions _examples/advanced-generic-openapi31/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ func NewRouter() http.Handler {
s.Post("/text-req-body/{path}", textReqBody(), nethttp.RequestBodyContent("text/csv"))
s.Post("/text-req-body-ptr/{path}", textReqBodyPtr(), nethttp.RequestBodyContent("text/csv"))

s.Post("/form-or-json/{path}", formOrJSON())

// Security middlewares.
// - sessMW is the actual request-level processor,
// - sessDoc is a handler-level wrapper to expose docs.
Expand Down
5 changes: 5 additions & 0 deletions request/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ func decodeValidate(d *form.Decoder, v interface{}, p url.Values, in rest.ParamI

func makeDecoder(in rest.ParamIn, formDecoder *form.Decoder, decoderFunc decoderFunc) valueDecoderFunc {
return func(r *http.Request, v interface{}, validator rest.Validator) error {
ct := r.Header.Get("Content-Type")
if in == rest.ParamInFormData && ct != "" && ct != "multipart/form-data" && ct != "application/x-www-form-urlencoded" {
return nil
}

values, err := decoderFunc(r)
if err != nil {
return err
Expand Down
37 changes: 37 additions & 0 deletions request/decoder_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package request_test

import (
"bytes"
"context"
"fmt"
"net/http"
Expand Down Expand Up @@ -566,3 +567,39 @@ func TestDecoderFactory_MakeDecoder_default_unexported(t *testing.T) {
dec := f.MakeDecoder(http.MethodGet, showImageInput{}, nil)
assert.NotNil(t, dec)
}

type formOrJSONInput struct {
Field1 string `json:"field1" formData:"field1" required:"true"`
Field2 int `json:"field2" formData:"field2" required:"true"`
}

func (formOrJSONInput) ForceJSONRequestBody() {}

func TestDecoderFactory_MakeDecoder_formOrJSON(t *testing.T) {
var in formOrJSONInput

dec := request.NewDecoderFactory().MakeDecoder(http.MethodPost, in, nil)

validator := jsonschema.NewFactory(&openapi.Collector{}, &openapi.Collector{}).
MakeRequestValidator(http.MethodPost, in, nil)

req, err := http.NewRequestWithContext(context.Background(), http.MethodPost,
"/", bytes.NewReader([]byte(`{"field1":"abc","field2":123}`)))
assert.NoError(t, err)

req.Header.Set("Content-Type", "application/json")

require.NoError(t, dec.Decode(req, &in, validator))
assert.Equal(t, "abc", in.Field1)
assert.Equal(t, 123, in.Field2)

in = formOrJSONInput{}
req, err = http.NewRequestWithContext(context.Background(), http.MethodPost,
"/", bytes.NewReader([]byte(`field1=abc&field2=123`)))
assert.NoError(t, err)

req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
require.NoError(t, dec.Decode(req, &in, validator))
assert.Equal(t, "abc", in.Field1)
assert.Equal(t, 123, in.Field2)
}

0 comments on commit 41ffc11

Please sign in to comment.