mirror of
				https://github.com/labstack/echo.git
				synced 2025-10-30 23:57:38 +02:00 
			
		
		
		
	* Fluent Binder for Query/Path/Form binding. * CI: report coverage for latest go (1.15) version * improve docs, remove uncommented code * separate unixtime with sec and nanosec precision binding
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/workflows/echo.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/echo.yml
									
									
									
									
										vendored
									
									
								
							| @@ -59,7 +59,7 @@ jobs: | ||||
|           go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./... | ||||
|  | ||||
|       - name: Upload coverage to Codecov | ||||
|         if: success() && matrix.go == 1.13 && matrix.os == 'ubuntu-latest' | ||||
|         if: success() && matrix.go == 1.15 && matrix.os == 'ubuntu-latest' | ||||
|         uses: codecov/codecov-action@v1 | ||||
|         with: | ||||
|           token: | ||||
|   | ||||
							
								
								
									
										7
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								Makefile
									
									
									
									
									
								
							| @@ -23,5 +23,12 @@ test: ## Run tests | ||||
| race: ## Run tests with data race detector | ||||
| 	@go test -race ${PKG_LIST} | ||||
|  | ||||
| benchmark: ## Run benchmarks | ||||
| 	@go test -run="-" -bench=".*" ${PKG_LIST} | ||||
|  | ||||
| help: ## Display this help screen | ||||
| 	@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' | ||||
|  | ||||
| goversion ?= "1.12" | ||||
| test_version: ## Run tests inside Docker with given version (defaults to 1.12 oldest supported). Example: make test_version goversion=1.13 | ||||
| 	@docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make check" | ||||
|   | ||||
							
								
								
									
										130
									
								
								binder_external_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								binder_external_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| // run tests as external package to get real feel for API | ||||
| package echo_test | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"github.com/labstack/echo/v4" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| ) | ||||
|  | ||||
| func ExampleValueBinder_BindErrors() { | ||||
| 	// example route function that binds query params to different destinations and returns all bind errors in one go | ||||
| 	routeFunc := func(c echo.Context) error { | ||||
| 		var opts struct { | ||||
| 			Active bool | ||||
| 			IDs    []int64 | ||||
| 		} | ||||
| 		length := int64(50) // default length is 50 | ||||
|  | ||||
| 		b := echo.QueryParamsBinder(c) | ||||
|  | ||||
| 		errs := b.Int64("length", &length). | ||||
| 			Int64s("ids", &opts.IDs). | ||||
| 			Bool("active", &opts.Active). | ||||
| 			BindErrors() // returns all errors | ||||
| 		if errs != nil { | ||||
| 			for _, err := range errs { | ||||
| 				bErr := err.(*echo.BindingError) | ||||
| 				log.Printf("in case you want to access what field: %s values: %v failed", bErr.Field, bErr.Values) | ||||
| 			} | ||||
| 			return fmt.Errorf("%v fields failed to bind", len(errs)) | ||||
| 		} | ||||
| 		fmt.Printf("active = %v, length = %v, ids = %v", opts.Active, length, opts.IDs) | ||||
|  | ||||
| 		return c.JSON(http.StatusOK, opts) | ||||
| 	} | ||||
|  | ||||
| 	e := echo.New() | ||||
| 	c := e.NewContext( | ||||
| 		httptest.NewRequest(http.MethodGet, "/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3", nil), | ||||
| 		httptest.NewRecorder(), | ||||
| 	) | ||||
|  | ||||
| 	_ = routeFunc(c) | ||||
|  | ||||
| 	// Output: active = true, length = 25, ids = [1 2 3] | ||||
| } | ||||
|  | ||||
| func ExampleValueBinder_BindError() { | ||||
| 	// example route function that binds query params to different destinations and stops binding on first bind error | ||||
| 	failFastRouteFunc := func(c echo.Context) error { | ||||
| 		var opts struct { | ||||
| 			Active bool | ||||
| 			IDs    []int64 | ||||
| 		} | ||||
| 		length := int64(50) // default length is 50 | ||||
|  | ||||
| 		// create binder that stops binding at first error | ||||
| 		b := echo.QueryParamsBinder(c) | ||||
|  | ||||
| 		err := b.Int64("length", &length). | ||||
| 			Int64s("ids", &opts.IDs). | ||||
| 			Bool("active", &opts.Active). | ||||
| 			BindError() // returns first binding error | ||||
| 		if err != nil { | ||||
| 			bErr := err.(*echo.BindingError) | ||||
| 			return fmt.Errorf("my own custom error for field: %s values: %v", bErr.Field, bErr.Values) | ||||
| 		} | ||||
| 		fmt.Printf("active = %v, length = %v, ids = %v\n", opts.Active, length, opts.IDs) | ||||
|  | ||||
| 		return c.JSON(http.StatusOK, opts) | ||||
| 	} | ||||
|  | ||||
| 	e := echo.New() | ||||
| 	c := e.NewContext( | ||||
| 		httptest.NewRequest(http.MethodGet, "/api/endpoint?active=true&length=25&ids=1&ids=2&ids=3", nil), | ||||
| 		httptest.NewRecorder(), | ||||
| 	) | ||||
|  | ||||
| 	_ = failFastRouteFunc(c) | ||||
|  | ||||
| 	// Output: active = true, length = 25, ids = [1 2 3] | ||||
| } | ||||
|  | ||||
| func ExampleValueBinder_CustomFunc() { | ||||
| 	// example route function that binds query params using custom function closure | ||||
| 	routeFunc := func(c echo.Context) error { | ||||
| 		length := int64(50) // default length is 50 | ||||
| 		var binary []byte | ||||
|  | ||||
| 		b := echo.QueryParamsBinder(c) | ||||
| 		errs := b.Int64("length", &length). | ||||
| 			CustomFunc("base64", func(values []string) []error { | ||||
| 				if len(values) == 0 { | ||||
| 					return nil | ||||
| 				} | ||||
| 				decoded, err := base64.URLEncoding.DecodeString(values[0]) | ||||
| 				if err != nil { | ||||
| 					// in this example we use only first param value but url could contain multiple params in reality and | ||||
| 					// therefore in theory produce multiple binding errors | ||||
| 					return []error{echo.NewBindingError("base64", values[0:1], "failed to decode base64", err)} | ||||
| 				} | ||||
| 				binary = decoded | ||||
| 				return nil | ||||
| 			}). | ||||
| 			BindErrors() // returns all errors | ||||
|  | ||||
| 		if errs != nil { | ||||
| 			for _, err := range errs { | ||||
| 				bErr := err.(*echo.BindingError) | ||||
| 				log.Printf("in case you want to access what field: %s values: %v failed", bErr.Field, bErr.Values) | ||||
| 			} | ||||
| 			return fmt.Errorf("%v fields failed to bind", len(errs)) | ||||
| 		} | ||||
| 		fmt.Printf("length = %v, base64 = %s", length, binary) | ||||
|  | ||||
| 		return c.JSON(http.StatusOK, "ok") | ||||
| 	} | ||||
|  | ||||
| 	e := echo.New() | ||||
| 	c := e.NewContext( | ||||
| 		httptest.NewRequest(http.MethodGet, "/api/endpoint?length=25&base64=SGVsbG8gV29ybGQ%3D", nil), | ||||
| 		httptest.NewRecorder(), | ||||
| 	) | ||||
| 	_ = routeFunc(c) | ||||
|  | ||||
| 	// Output: length = 25, base64 = Hello World | ||||
| } | ||||
							
								
								
									
										265
									
								
								binder_go1.15_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								binder_go1.15_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,265 @@ | ||||
| // +build go1.15 | ||||
|  | ||||
| package echo | ||||
|  | ||||
| /** | ||||
| 	Since version 1.15 time.Time and time.Duration error message pattern has changed (values are wrapped now in \"\") | ||||
| 	So pre 1.15 these tests fail with similar error: | ||||
|  | ||||
|   expected: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param" | ||||
|   actual  : "code=400, message=failed to bind field value to Duration, internal=time: invalid duration nope, field=param" | ||||
| */ | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func createTestContext15(URL string, body io.Reader, pathParams map[string]string) Context { | ||||
| 	e := New() | ||||
| 	req := httptest.NewRequest(http.MethodGet, URL, body) | ||||
| 	if body != nil { | ||||
| 		req.Header.Set(HeaderContentType, MIMEApplicationJSON) | ||||
| 	} | ||||
| 	rec := httptest.NewRecorder() | ||||
| 	c := e.NewContext(req, rec) | ||||
|  | ||||
| 	if len(pathParams) > 0 { | ||||
| 		names := make([]string, 0) | ||||
| 		values := make([]string, 0) | ||||
| 		for name, value := range pathParams { | ||||
| 			names = append(names, name) | ||||
| 			values = append(values, value) | ||||
| 		} | ||||
| 		c.SetParamNames(names...) | ||||
| 		c.SetParamValues(values...) | ||||
| 	} | ||||
|  | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func TestValueBinder_TimeError(t *testing.T) { | ||||
| 	var testCases = []struct { | ||||
| 		name            string | ||||
| 		givenFailFast   bool | ||||
| 		givenBindErrors []error | ||||
| 		whenURL         string | ||||
| 		whenMust        bool | ||||
| 		whenLayout      string | ||||
| 		expectValue     time.Time | ||||
| 		expectError     string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:        "nok, conversion fails, value is not changed", | ||||
| 			whenURL:     "/search?param=nope¶m=100", | ||||
| 			expectValue: time.Time{}, | ||||
| 			expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\": extra text: \"nope\", field=param", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "nok (must), conversion fails, value is not changed", | ||||
| 			whenMust:    true, | ||||
| 			whenURL:     "/search?param=nope¶m=100", | ||||
| 			expectValue: time.Time{}, | ||||
| 			expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\": extra text: \"nope\", field=param", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			c := createTestContext15(tc.whenURL, nil, nil) | ||||
| 			b := QueryParamsBinder(c).FailFast(tc.givenFailFast) | ||||
| 			if tc.givenFailFast { | ||||
| 				b.errors = []error{errors.New("previous error")} | ||||
| 			} | ||||
|  | ||||
| 			dest := time.Time{} | ||||
| 			var err error | ||||
| 			if tc.whenMust { | ||||
| 				err = b.MustTime("param", &dest, tc.whenLayout).BindError() | ||||
| 			} else { | ||||
| 				err = b.Time("param", &dest, tc.whenLayout).BindError() | ||||
| 			} | ||||
|  | ||||
| 			assert.Equal(t, tc.expectValue, dest) | ||||
| 			if tc.expectError != "" { | ||||
| 				assert.EqualError(t, err, tc.expectError) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValueBinder_TimesError(t *testing.T) { | ||||
| 	var testCases = []struct { | ||||
| 		name            string | ||||
| 		givenFailFast   bool | ||||
| 		givenBindErrors []error | ||||
| 		whenURL         string | ||||
| 		whenMust        bool | ||||
| 		whenLayout      string | ||||
| 		expectValue     []time.Time | ||||
| 		expectError     string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:          "nok, fail fast without binding value", | ||||
| 			givenFailFast: true, | ||||
| 			whenURL:       "/search?param=1¶m=100", | ||||
| 			expectValue:   []time.Time(nil), | ||||
| 			expectError:   "code=400, message=failed to bind field value to Time, internal=parsing time \"1\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"1\" as \"2006\", field=param", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "nok, conversion fails, value is not changed", | ||||
| 			whenURL:     "/search?param=nope¶m=100", | ||||
| 			expectValue: []time.Time(nil), | ||||
| 			expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "nok (must), conversion fails, value is not changed", | ||||
| 			whenMust:    true, | ||||
| 			whenURL:     "/search?param=nope¶m=100", | ||||
| 			expectValue: []time.Time(nil), | ||||
| 			expectError: "code=400, message=failed to bind field value to Time, internal=parsing time \"nope\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"nope\" as \"2006\", field=param", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			c := createTestContext15(tc.whenURL, nil, nil) | ||||
| 			b := QueryParamsBinder(c).FailFast(tc.givenFailFast) | ||||
| 			b.errors = tc.givenBindErrors | ||||
|  | ||||
| 			layout := time.RFC3339 | ||||
| 			if tc.whenLayout != "" { | ||||
| 				layout = tc.whenLayout | ||||
| 			} | ||||
|  | ||||
| 			var dest []time.Time | ||||
| 			var err error | ||||
| 			if tc.whenMust { | ||||
| 				err = b.MustTimes("param", &dest, layout).BindError() | ||||
| 			} else { | ||||
| 				err = b.Times("param", &dest, layout).BindError() | ||||
| 			} | ||||
|  | ||||
| 			assert.Equal(t, tc.expectValue, dest) | ||||
| 			if tc.expectError != "" { | ||||
| 				assert.EqualError(t, err, tc.expectError) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValueBinder_DurationError(t *testing.T) { | ||||
| 	var testCases = []struct { | ||||
| 		name            string | ||||
| 		givenFailFast   bool | ||||
| 		givenBindErrors []error | ||||
| 		whenURL         string | ||||
| 		whenMust        bool | ||||
| 		expectValue     time.Duration | ||||
| 		expectError     string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:        "nok, conversion fails, value is not changed", | ||||
| 			whenURL:     "/search?param=nope¶m=100", | ||||
| 			expectValue: 0, | ||||
| 			expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "nok (must), conversion fails, value is not changed", | ||||
| 			whenMust:    true, | ||||
| 			whenURL:     "/search?param=nope¶m=100", | ||||
| 			expectValue: 0, | ||||
| 			expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			c := createTestContext15(tc.whenURL, nil, nil) | ||||
| 			b := QueryParamsBinder(c).FailFast(tc.givenFailFast) | ||||
| 			if tc.givenFailFast { | ||||
| 				b.errors = []error{errors.New("previous error")} | ||||
| 			} | ||||
|  | ||||
| 			var dest time.Duration | ||||
| 			var err error | ||||
| 			if tc.whenMust { | ||||
| 				err = b.MustDuration("param", &dest).BindError() | ||||
| 			} else { | ||||
| 				err = b.Duration("param", &dest).BindError() | ||||
| 			} | ||||
|  | ||||
| 			assert.Equal(t, tc.expectValue, dest) | ||||
| 			if tc.expectError != "" { | ||||
| 				assert.EqualError(t, err, tc.expectError) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestValueBinder_DurationsError(t *testing.T) { | ||||
| 	var testCases = []struct { | ||||
| 		name            string | ||||
| 		givenFailFast   bool | ||||
| 		givenBindErrors []error | ||||
| 		whenURL         string | ||||
| 		whenMust        bool | ||||
| 		expectValue     []time.Duration | ||||
| 		expectError     string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:          "nok, fail fast without binding value", | ||||
| 			givenFailFast: true, | ||||
| 			whenURL:       "/search?param=1¶m=100", | ||||
| 			expectValue:   []time.Duration(nil), | ||||
| 			expectError:   "code=400, message=failed to bind field value to Duration, internal=time: missing unit in duration \"1\", field=param", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "nok, conversion fails, value is not changed", | ||||
| 			whenURL:     "/search?param=nope¶m=100", | ||||
| 			expectValue: []time.Duration(nil), | ||||
| 			expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "nok (must), conversion fails, value is not changed", | ||||
| 			whenMust:    true, | ||||
| 			whenURL:     "/search?param=nope¶m=100", | ||||
| 			expectValue: []time.Duration(nil), | ||||
| 			expectError: "code=400, message=failed to bind field value to Duration, internal=time: invalid duration \"nope\", field=param", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			c := createTestContext15(tc.whenURL, nil, nil) | ||||
| 			b := QueryParamsBinder(c).FailFast(tc.givenFailFast) | ||||
| 			b.errors = tc.givenBindErrors | ||||
|  | ||||
| 			var dest []time.Duration | ||||
| 			var err error | ||||
| 			if tc.whenMust { | ||||
| 				err = b.MustDurations("param", &dest).BindError() | ||||
| 			} else { | ||||
| 				err = b.Durations("param", &dest).BindError() | ||||
| 			} | ||||
|  | ||||
| 			assert.Equal(t, tc.expectValue, dest) | ||||
| 			if tc.expectError != "" { | ||||
| 				assert.EqualError(t, err, tc.expectError) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										2757
									
								
								binder_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2757
									
								
								binder_test.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user