Repository: gin-gonic/gin
Pull request #3962 (2024-05-09 19:38:58)

docs(readme): fix language and moved link
I tried to make the README a little more readable and also replaced a moved link


Patch:
diff --git a/Makefile b/Makefile
index b58f24f..1a7de86 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,7 @@ TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | gr
 TESTTAGS ?= ""
 
 .PHONY: test
+# Run tests to verify code functionality.
 test:
 	echo "mode: count" > coverage.out
 	for d in $(TESTFOLDER); do \
@@ -30,10 +31,12 @@ test:
 	done
 
 .PHONY: fmt
+# Ensure consistent code formatting.
 fmt:
 	$(GOFMT) -w $(GOFILES)
 
 .PHONY: fmt-check
+# format (check only).
 fmt-check:
 	@diff=$$($(GOFMT) -d $(GOFILES)); \
 	if [ -n "$$diff" ]; then \
@@ -43,31 +46,36 @@ fmt-check:
 	fi;
 
 .PHONY: vet
+# Examine packages and report suspicious constructs if any.
 vet:
 	$(GO) vet $(VETPACKAGES)
 
 .PHONY: lint
+# Inspect source code for stylistic errors or potential bugs.
 lint:
 	@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
 		$(GO) get -u golang.org/x/lint/golint; \
 	fi
 	for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
 
-.PHONY: misspell-check
-misspell-check:
+.PHONY: misspell
+# Correct commonly misspelled English words in source code.
+misspell:
 	@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
 		$(GO) get -u github.com/client9/misspell/cmd/misspell; \
 	fi
-	misspell -error $(GOFILES)
+	misspell -w $(GOFILES)
 
-.PHONY: misspell
-misspell:
+.PHONY: misspell-check
+# misspell (check only).
+misspell-check:
 	@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
 		$(GO) get -u github.com/client9/misspell/cmd/misspell; \
 	fi
-	misspell -w $(GOFILES)
+	misspell -error $(GOFILES)
 
 .PHONY: tools
+# Install tools (golint and misspell).
 tools:
 	@if [ $(GO_VERSION) -gt 15 ]; then \
 		$(GO) install golang.org/x/lint/golint@latest; \
@@ -76,3 +84,23 @@ tools:
 		$(GO) install golang.org/x/lint/golint; \
 		$(GO) install github.com/client9/misspell/cmd/misspell; \
 	fi
+
+.PHONY: help
+# Help.
+help:
+	@echo ''
+	@echo 'Usage:'
+	@echo ' make [target]'
+	@echo ''
+	@echo 'Targets:'
+	@awk '/^[a-zA-Z\-\0-9]+:/ { \
+	helpMessage = match(lastLine, /^# (.*)/); \
+		if (helpMessage) { \
+			helpCommand = substr($$1, 0, index($$1, ":")-1); \
+			helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
+			printf " - \033[36m%-20s\033[0m %s\n", helpCommand, helpMessage; \
+		} \
+	} \
+	{ lastLine = $$0 }' $(MAKEFILE_LIST)
+
+.DEFAULT_GOAL := help
diff --git a/README.md b/README.md
index 04217d6..a595656 100644
--- a/README.md
+++ b/README.md
@@ -11,37 +11,36 @@
 [![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
 [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/gin-gonic/gin)](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin)
 
-Gin is a web framework written in [Go](https://go.dev/). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
+Gin is a web framework written in [Go](https://go.dev/). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter).
+If you need performance and good productivity, you will love Gin.
 
-**The key features of Gin are:**
+**Gin's key features are:**
 
 - Zero allocation router
-- Fast
+- Speed
 - Middleware support
 - Crash-free
 - JSON validation
-- Routes grouping
+- Route grouping
 - Error management
-- Rendering built-in
-- Extendable
+- Built-in rendering
+- Extensible
 
 ## Getting started
 
 ### Prerequisites
 
-The required version of [Go](https://go.dev/) language is [1.21](https://go.dev/doc/devel/release#go1.21.0) or above.
+Gin requires [Go](https://go.dev/) version [1.21](https://go.dev/doc/devel/release#go1.21.0) or above.
 
 ### Getting Gin
 
-With [Go module](https://github.com/golang/go/wiki/Modules) support, simply add the following import
+With [Go's module support](https://go.dev/wiki/Modules#how-to-use-modules), `go [build|run|test]` automatically fetches the necessary dependencies when you add the import in your code:
 
 ```sh
 import "github.com/gin-gonic/gin"
 ```
 
-to your code, and then `go [build|run|test]` will automatically fetch the necessary dependencies.
-
-Otherwise, run the following Go command to install the `gin` package:
+Alternatively, use `go get`:
 
 ```sh
 go get -u github.com/gin-gonic/gin
@@ -49,7 +48,7 @@ go get -u github.com/gin-gonic/gin
 
 ### Running Gin
 
-First you need to import Gin package for using Gin, one simplest example likes the follow `example.go`:
+A basic example:
 
 ```go
 package main
@@ -71,28 +70,29 @@ func main() {
 }
 ```
 
-And use the Go command to run the demo:
+To run the code, use the `go run` command, like:
 
 ```sh
-# run example.go and visit 0.0.0.0:8080/ping on browser
 $ go run example.go
 ```
 
-### Learn more examples
+Then visit [`0.0.0.0:8080/ping`](http://0.0.0.0:8080/ping) in your browser to see the response!
+
+### See more examples
 
 #### Quick Start
 
-Learn and practice more examples, please read the [Gin Quick Start](docs/doc.md) which includes API examples and builds tag.
+Learn and practice with the [Gin Quick Start](docs/doc.md), which includes API examples and builds tag.
 
 #### Examples
 
-A number of ready-to-run examples demonstrating various use cases of Gin on the [Gin examples](https://github.com/gin-gonic/examples) repository.
+A number of ready-to-run examples demonstrating various use cases of Gin are available in the [Gin examples](https://github.com/gin-gonic/examples) repository.
 
 ## Documentation
 
-See [API documentation and descriptions](https://godoc.org/github.com/gin-gonic/gin) for package.
+See the [API documentation on godoc.org](https://godoc.org/github.com/gin-gonic/gin).
 
-All documentation is available on the Gin website.
+The documentation is also available on [gin-gonic.com](https://gin-gonic.com) in several languages:
 
 - [English](https://gin-gonic.com/docs/)
 - [简体中文](https://gin-gonic.com/zh-cn/docs/)
@@ -103,15 +103,13 @@ All documentation is available on the Gin website.
 - [Turkish](https://gin-gonic.com/tr/docs/)
 - [Persian](https://gin-gonic.com/fa/docs/)
 
-### Articles about Gin
-
-A curated list of awesome Gin framework.
+### Articles
 
 - [Tutorial: Developing a RESTful API with Go and Gin](https://go.dev/doc/tutorial/web-service-gin)
 
 ## Benchmarks
 
-Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks details](/BENCHMARKS.md).
+Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter), [see all benchmarks](/BENCHMARKS.md).
 
 | Benchmark name                 |       (1) |             (2) |          (3) |             (4) |
 | ------------------------------ | ---------:| ---------------:| ------------:| ---------------:|
@@ -151,23 +149,23 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
 - (3): Heap Memory (B/op), lower is better
 - (4): Average Allocations per Repetition (allocs/op), lower is better
 
-## Middlewares
+## Middleware
 
 You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib).
 
-## Users
+## Uses
 
-Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
+Here are some awesome projects that are using the [Gin](https://github.com/gin-gonic/gin) web framework.
 
-- [gorush](https://github.com/appleboy/gorush): A push notification server written in Go.
-- [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform.
-- [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
-- [lura](https://github.com/luraproject/lura): Ultra performant API Gateway with middlewares.
-- [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
+- [gorush](https://github.com/appleboy/gorush): A push notification server.
+- [fnproject](https://github.com/fnproject/fn): A container native, cloud agnostic serverless platform.
+- [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Google TensorFlow.
+- [lura](https://github.com/luraproject/lura): Ultra performant API Gateway with middleware.
+- [picfit](https://github.com/thoas/picfit): An image resizing server.
 - [dkron](https://github.com/distribworks/dkron): Distributed, fault tolerant job scheduling system.
 
 ## Contributing
 
 Gin is the work of hundreds of contributors. We appreciate your help!
 
-Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
+Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
diff --git a/binding/binding.go b/binding/binding.go
index 9472387..702d0e8 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 552a86b..c8e6131 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/plain.go b/binding/plain.go
new file mode 100644
index 0000000..3b250bb
--- /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 391adaf..3525066 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/go.mod b/go.mod
index 6063f3b..4937d2b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/gin-gonic/gin
 
-go 1.21
+go 1.21.0
 
 require (
 	github.com/bytedance/sonic v1.11.6


Test patch:
diff --git a/binding/binding_test.go b/binding/binding_test.go
index feb8eed..c59e5e9 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/context_test.go b/context_test.go
index ae34c65..36d6e34 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: `<?xml version="1.0" encoding="UTF-8"?>
+<root>
+<foo>FOO</foo>
+</root>`,
+		},
+		{
+			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\"}"))