Skip to content

Commit

Permalink
global: add error to bind, add interface to bind, add default binder …
Browse files Browse the repository at this point in the history
…to not reallocate it
  • Loading branch information
alexisvisco committed Sep 4, 2023
1 parent e187bd9 commit 8330ba1
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 58 deletions.
10 changes: 5 additions & 5 deletions exemple/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ module github.com/caumette-co/x/example
go 1.21.0

replace github.com/caumette-co/x/xfoundation => ../xfoundation

replace github.com/caumette-co/x/xweb => ../xweb

require (
github.com/caumette-co/x/xfoundation v0.0.0-00010101000000-000000000000 // indirect
github.com/caumette-co/x/xweb v0.0.0-00010101000000-000000000000 // indirect
github.com/caumette-co/x/xfoundation v0.0.0-00010101000000-000000000000
github.com/caumette-co/x/xweb v0.0.0-00010101000000-000000000000
)

require (
github.com/go-chi/chi v1.5.4 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/samber/lo v1.38.1 // indirect
go.uber.org/dig v1.17.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
Expand Down
68 changes: 64 additions & 4 deletions exemple/go.sum
Original file line number Diff line number Diff line change
@@ -1,14 +1,74 @@
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/gavv/httpexpect/v2 v2.15.0 h1:CCnFk9of4l4ijUhnMxyoEpJsIIBKcuWIFLMwwGTZxNs=
github.com/gavv/httpexpect/v2 v2.15.0/go.mod h1:7myOP3A3VyS4+qnA4cm8DAad8zMN+7zxDB80W9f8yIc=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI=
go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE=
5 changes: 5 additions & 0 deletions exemple/http-client.env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dev": {
"url": "http://localhost:3000"
}
}
1 change: 1 addition & 0 deletions exemple/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ func Routes(router *xweb.Router) {
router.Get("/direct", handler.HandleDirect)
router.Get("/new", xweb.Handler[any](handler.HandleNew))
router.Get("/new2", handler.HandleNew2)
router.Get("/contact", xweb.Handler[handler.Contact](handler.HandleContact))
}
14 changes: 14 additions & 0 deletions exemple/internal/app/handler/handle_home.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package handler

import (
"fmt"
"github.com/caumette-co/x/xweb"
"net/http"
)
Expand Down Expand Up @@ -35,3 +36,16 @@ func HandleNew2() xweb.Handler[any] {
}, nil
}
}

type Contact struct {
Email string `query:"email"`
}

func HandleContact(r *xweb.Request[Contact]) (xweb.Response, error) {
fmt.Println(r.Params().Email)

return xweb.JSONResponse{
StatusCode: http.StatusOK,
Payload: map[string]interface{}{"hello": true},
}, nil
}
3 changes: 3 additions & 0 deletions exemple/tests.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### GET request with a header
GET {{url}}/[email protected]
Accept: application/json
63 changes: 30 additions & 33 deletions xweb/binding/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"github.com/samber/lo"
"io"
Expand All @@ -13,12 +12,17 @@ import (
"strings"
)

var ErrInvalidParam = errors.New("invalid params: only ptr to a struct or string are accepted")
var Default = NewBinder(
StringsParamExtractors,
ValuesParamExtractors,
)

var MaxBodySize = int64(256 * 1024)
type Binder interface {
Bind(request *http.Request, params any) error
}

// Binder allow to bind params from a request to a struct.
type Binder struct {
// bind allow to bind params from a request to a struct.
type bind struct {
structCaches map[reflect.Type]StructCache

stringsExtractors []StringsParamExtractor
Expand All @@ -28,8 +32,8 @@ type Binder struct {
valuesTags []string
}

func NewBinder(stringsExtractors []StringsParamExtractor, valueExtractors []ValueParamExtractor) *Binder {
b := Binder{
func NewBinder(stringsExtractors []StringsParamExtractor, valueExtractors []ValueParamExtractor) Binder {
b := bind{
stringsExtractors: stringsExtractors,
valueExtractors: valueExtractors,

Expand All @@ -52,7 +56,7 @@ func NewBinder(stringsExtractors []StringsParamExtractor, valueExtractors []Valu
return &b
}

func (b *Binder) Bind(request *http.Request, params any) []error {
func (b *bind) Bind(request *http.Request, params any) error {
errors := make([]error, 0)

dec := NewDecoder(
Expand All @@ -61,8 +65,8 @@ func (b *Binder) Bind(request *http.Request, params any) []error {
b.valueExtractors,
)

if err := validateParam(params); err != nil {
return []error{err}
if err := b.isPointerToStruct(params); err != nil {
return &BindingError{Errors: []error{err}}
}

paramsType := reflect.TypeOf(params)
Expand All @@ -71,31 +75,26 @@ func (b *Binder) Bind(request *http.Request, params any) []error {
errors = append(errors, err)
}

if reflect.ValueOf(params).Kind() != reflect.String {
var (
structCache StructCache
ok bool
)
var (
structCache StructCache
ok bool
)

if structCache, ok = b.structCaches[paramsType]; !ok {
structCache = NewStructAnalyzer(b.stringsTags, b.valuesTags, paramsType).Cache()
b.structCaches[paramsType] = structCache
}
if structCache, ok = b.structCaches[paramsType]; !ok {
structCache = NewStructAnalyzer(b.stringsTags, b.valuesTags, paramsType).Cache()
b.structCaches[paramsType] = structCache
}

if errs := dec.Decode(structCache, reflect.ValueOf(params)); errs != nil {
errors = append(errors, errs...)
}
if errs := dec.Decode(structCache, reflect.ValueOf(params)); errs != nil {
errors = append(errors, errs...)
}

return errors
return &BindingError{Errors: errors}

}

// validateParam validate if the param is valid.
// Accepted values :
// - pointer to a struct
// - string
func validateParam(param any) error {
// isPointerToStruct ensure that the params is a pointer to a struct
func (b *bind) isPointerToStruct(param any) error {
ref := reflect.ValueOf(param)

if ref.Kind() == reflect.Ptr {
Expand All @@ -106,19 +105,17 @@ func validateParam(param any) error {
}
}

if ref.Kind() == reflect.String {
return nil
}

return ValidateParamsError{error: ErrInvalidParam}
}

var MaxBodySize = int64(256 * 1024)

// bindBody bind the body of the request to the params.
// it supports 3 types of content-type:
// - application/json
// - application/xml
// - text/plain
func (b *Binder) bindBody(r *http.Request, params any) error {
func (b *bind) bindBody(r *http.Request, params any) error {
if r.ContentLength == 0 {
return nil
}
Expand Down
6 changes: 3 additions & 3 deletions xweb/binding/bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestBinder_Bind(t *testing.T) {
r = r.WithContext(context.WithValue(r.Context(), "fromContext", "value"))

var test testStruct
err := binder.Bind(r, w, &test)
err := binder.Bind(r, &test)
require.Len(t, err, 0)

require.Equal(t, "value", test.FromPath)
Expand Down Expand Up @@ -70,7 +70,7 @@ func TestBinder_Bind(t *testing.T) {

chi.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var test testStruct
err := binder.Bind(r, w, &test)
err := binder.Bind(r, &test)
require.Len(t, err, 0)

require.Equal(t, "value", test.FromForm)
Expand Down Expand Up @@ -105,7 +105,7 @@ func TestBinder_Bind(t *testing.T) {

chi.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var test testStruct
err := binder.Bind(r, w, &test)
err := binder.Bind(r, &test)
require.Len(t, err, 1)

require.IsType(t, &FieldSetterError{}, err[0])
Expand Down
28 changes: 26 additions & 2 deletions xweb/binding/errors.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
package binding

import "fmt"
import (
"errors"
"fmt"
)

// ValidateParamsError is the error returned when a params is not either a pointer to a struct or a string.
var ErrInvalidParam = errors.New("invalid params: only ptr to a struct")

// ValidateParamsError is the error returned when a params is not either a pointer to a struct
type ValidateParamsError struct {
error
}

// BindBodyError is the error returned when the body can't be binded.
type BindBodyError struct {
error
ContentType string
}

// ExtractError is the error returned when a value can't be extracted from the request.
type ExtractError struct {
error
Tag string
}

// FieldSetterError is the error returned when a value can't be set to the params.
type FieldSetterError struct {
FieldSetterContext FieldSetterContext
Message string
}

// Error returns the error message.
func (f FieldSetterError) Error() string {
return fmt.Sprintf("%s: %v", f.Message, f.FieldSetterContext)
}
Expand All @@ -34,3 +43,18 @@ type FieldSetterContext struct {
ValueIndex int `json:"value_index,omitempty"`
DecodingStrategy string `json:"decoding_strategy,omitempty"`
}

type BindingError struct {
// Errors Can be one of : ValidateParamsError, BindBodyError, ExtractError, FieldSetterError
Errors []error
}

func (b BindingError) Error() string {
str := ""

for _, err := range b.Errors {
str += err.Error() + "\n"
}

return str
}
12 changes: 11 additions & 1 deletion xweb/handler.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package xweb

import (
"github.com/caumette-co/x/xweb/binding"
"net/http"
)

type Handler[P any] func(r *Request[P]) (Response, error)

func (h Handler[P]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
xr := &Request[P]{Request: r}
xr := newRequest[P](r, binding.Default) // TODO: inject binder in some way to allow customize it binder
response, err := h(xr)
if err != nil {
// todo:
// - read accept content type
//if wrappedError, ok := err.(*errorWithResponse); ok {
// response = wrappedError.response
//} else {
Expand All @@ -19,6 +22,13 @@ func (h Handler[P]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Payload: ErrorPayload{Message: http.StatusText(http.StatusInternalServerError)},
// }
//}

// TODO: handle error properly

w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))

return
}

if response == nil {
Expand Down
3 changes: 2 additions & 1 deletion xweb/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import (
)

type Provider struct {
Addr string
Addr string

Routes func(router *Router)
}

Expand Down
Loading

0 comments on commit 8330ba1

Please sign in to comment.