From 6ca8ddb1aed78d9ffaf984e5489111838242fedb Mon Sep 17 00:00:00 2001 From: guonaihong Date: Mon, 13 May 2024 11:11:56 +0800 Subject: [PATCH] feat(binding): add BindPlain (#3904) * add BindPlain * fix ci/cd error --- binding/binding.go | 1 + binding/binding_nomsgpack.go | 1 + binding/binding_test.go | 40 +++++++++++ binding/plain.go | 56 ++++++++++++++++ context.go | 17 ++++- context_test.go | 124 +++++++++++++++++++++++++++++++++++ 6 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 binding/plain.go diff --git a/binding/binding.go b/binding/binding.go index 94723879a7..702d0e82f6 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -84,6 +84,7 @@ var ( YAML BindingBody = yamlBinding{} Uri BindingUri = uriBinding{} Header Binding = headerBinding{} + Plain BindingBody = plainBinding{} TOML BindingBody = tomlBinding{} ) diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go index 552a86b2d7..c8e6131012 100644 --- a/binding/binding_nomsgpack.go +++ b/binding/binding_nomsgpack.go @@ -81,6 +81,7 @@ var ( Uri = uriBinding{} Header = headerBinding{} TOML = tomlBinding{} + Plain = plainBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method diff --git a/binding/binding_test.go b/binding/binding_test.go index feb8eed558..c59e5e939a 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -1342,6 +1342,46 @@ func (h hook) Read([]byte) (int, error) { return 0, errors.New("error") } +type failRead struct{} + +func (f *failRead) Read(b []byte) (n int, err error) { + return 0, errors.New("my fail") +} + +func (f *failRead) Close() error { + return nil +} + +func TestPlainBinding(t *testing.T) { + p := Plain + assert.Equal(t, "plain", p.Name()) + + var s string + req := requestWithBody("POST", "/", "test string") + assert.NoError(t, p.Bind(req, &s)) + assert.Equal(t, s, "test string") + + var bs []byte + req = requestWithBody("POST", "/", "test []byte") + assert.NoError(t, p.Bind(req, &bs)) + assert.Equal(t, bs, []byte("test []byte")) + + var i int + req = requestWithBody("POST", "/", "test fail") + assert.Error(t, p.Bind(req, &i)) + + req = requestWithBody("POST", "/", "") + req.Body = &failRead{} + assert.Error(t, p.Bind(req, &s)) + + req = requestWithBody("POST", "/", "") + assert.Nil(t, p.Bind(req, nil)) + + var ptr *string + req = requestWithBody("POST", "/", "") + assert.Nil(t, p.Bind(req, ptr)) +} + func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) diff --git a/binding/plain.go b/binding/plain.go new file mode 100644 index 0000000000..3b250bb07a --- /dev/null +++ b/binding/plain.go @@ -0,0 +1,56 @@ +package binding + +import ( + "fmt" + "io" + "net/http" + "reflect" + + "github.com/gin-gonic/gin/internal/bytesconv" +) + +type plainBinding struct{} + +func (plainBinding) Name() string { + return "plain" +} + +func (plainBinding) Bind(req *http.Request, obj interface{}) error { + all, err := io.ReadAll(req.Body) + if err != nil { + return err + } + + return decodePlain(all, obj) +} + +func (plainBinding) BindBody(body []byte, obj any) error { + return decodePlain(body, obj) +} + +func decodePlain(data []byte, obj any) error { + if obj == nil { + return nil + } + + v := reflect.ValueOf(obj) + + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return nil + } + v = v.Elem() + } + + if v.Kind() == reflect.String { + v.SetString(bytesconv.BytesToString(data)) + return nil + } + + if _, ok := v.Interface().([]byte); ok { + v.SetBytes(data) + return nil + } + + return fmt.Errorf("type (%T) unknown type", v) +} diff --git a/context.go b/context.go index 391adafed0..3525066752 100644 --- a/context.go +++ b/context.go @@ -614,7 +614,7 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error } defer src.Close() - if err = os.MkdirAll(filepath.Dir(dst), 0750); err != nil { + if err = os.MkdirAll(filepath.Dir(dst), 0o750); err != nil { return err } @@ -667,6 +667,11 @@ func (c *Context) BindTOML(obj any) error { return c.MustBindWith(obj, binding.TOML) } +// BindPlain is a shortcut for c.MustBindWith(obj, binding.Plain). +func (c *Context) BindPlain(obj any) error { + return c.MustBindWith(obj, binding.Plain) +} + // BindHeader is a shortcut for c.MustBindWith(obj, binding.Header). func (c *Context) BindHeader(obj any) error { return c.MustBindWith(obj, binding.Header) @@ -732,6 +737,11 @@ func (c *Context) ShouldBindTOML(obj any) error { return c.ShouldBindWith(obj, binding.TOML) } +// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain). +func (c *Context) ShouldBindPlain(obj any) error { + return c.ShouldBindWith(obj, binding.Plain) +} + // ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header). func (c *Context) ShouldBindHeader(obj any) error { return c.ShouldBindWith(obj, binding.Header) @@ -794,6 +804,11 @@ func (c *Context) ShouldBindBodyWithTOML(obj any) error { return c.ShouldBindBodyWith(obj, binding.TOML) } +// ShouldBindBodyWithJSON is a shortcut for c.ShouldBindBodyWith(obj, binding.JSON). +func (c *Context) ShouldBindBodyWithPlain(obj any) error { + return c.ShouldBindBodyWith(obj, binding.Plain) +} + // ClientIP implements one best effort algorithm to return the real client IP. // It calls c.RemoteIP() under the hood, to check if the remote IP is a trusted proxy or not. // If it is it will then try to parse the headers defined in Engine.RemoteIPHeaders (defaulting to [X-Forwarded-For, X-Real-Ip]). diff --git a/context_test.go b/context_test.go index ae34c6592e..36d6e34ac4 100644 --- a/context_test.go +++ b/context_test.go @@ -1670,6 +1670,31 @@ func TestContextBindWithXML(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextBindPlain(t *testing.T) { + + // string + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`test string`)) + c.Request.Header.Add("Content-Type", MIMEPlain) + + var s string + + assert.NoError(t, c.BindPlain(&s)) + assert.Equal(t, "test string", s) + assert.Equal(t, 0, w.Body.Len()) + + // []byte + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`test []byte`)) + c.Request.Header.Add("Content-Type", MIMEPlain) + + var bs []byte + + assert.NoError(t, c.BindPlain(&bs)) + assert.Equal(t, []byte("test []byte"), bs) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextBindHeader(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -1816,6 +1841,31 @@ func TestContextShouldBindWithXML(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextShouldBindPlain(t *testing.T) { + // string + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`test string`)) + c.Request.Header.Add("Content-Type", MIMEPlain) + + var s string + + assert.NoError(t, c.ShouldBindPlain(&s)) + assert.Equal(t, "test string", s) + assert.Equal(t, 0, w.Body.Len()) + // []byte + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`test []byte`)) + c.Request.Header.Add("Content-Type", MIMEPlain) + + var bs []byte + + assert.NoError(t, c.ShouldBindPlain(&bs)) + assert.Equal(t, []byte("test []byte"), bs) + assert.Equal(t, 0, w.Body.Len()) + +} + func TestContextShouldBindHeader(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -2247,6 +2297,80 @@ func TestContextShouldBindBodyWithTOML(t *testing.T) { } } +func TestContextShouldBindBodyWithPlain(t *testing.T) { + for _, tt := range []struct { + name string + bindingBody binding.BindingBody + body string + }{ + { + name: " JSON & JSON-BODY ", + bindingBody: binding.JSON, + body: `{"foo":"FOO"}`, + }, + { + name: " JSON & XML-BODY ", + bindingBody: binding.XML, + body: ` + +FOO +`, + }, + { + name: " JSON & YAML-BODY ", + bindingBody: binding.YAML, + body: `foo: FOO`, + }, + { + name: " JSON & TOM-BODY ", + bindingBody: binding.TOML, + body: `foo=FOO`, + }, + { + name: " JSON & Plain-BODY ", + bindingBody: binding.Plain, + body: `foo=FOO`, + }, + } { + t.Logf("testing: %s", tt.name) + + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(tt.body)) + + type typeJSON struct { + Foo string `json:"foo" binding:"required"` + } + objJSON := typeJSON{} + + if tt.bindingBody == binding.Plain { + body := "" + assert.NoError(t, c.ShouldBindBodyWithPlain(&body)) + assert.Equal(t, body, "foo=FOO") + } + + if tt.bindingBody == binding.JSON { + assert.NoError(t, c.ShouldBindBodyWithJSON(&objJSON)) + assert.Equal(t, typeJSON{"FOO"}, objJSON) + } + + if tt.bindingBody == binding.XML { + assert.Error(t, c.ShouldBindBodyWithJSON(&objJSON)) + assert.Equal(t, typeJSON{}, objJSON) + } + + if tt.bindingBody == binding.YAML { + assert.Error(t, c.ShouldBindBodyWithJSON(&objJSON)) + assert.Equal(t, typeJSON{}, objJSON) + } + + if tt.bindingBody == binding.TOML { + assert.Error(t, c.ShouldBindBodyWithJSON(&objJSON)) + assert.Equal(t, typeJSON{}, objJSON) + } + } +} func TestContextGolangContext(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))