Skip to content

Commit

Permalink
Request wrapper remove http.Request
Browse files Browse the repository at this point in the history
  • Loading branch information
glossd committed Dec 8, 2024
1 parent 8735d26 commit 6b208d3
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 14 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,21 +312,21 @@ If you need to access http request attributes wrap the input with `fetch.Request
type Pet struct {
Name string
}
http.HandleFunc("/pets", fetch.ToHandlerFunc(func(in fetch.Request[Pet]) (*fetch.Empty, error) {
fmt.Println("Request context:", in.Context())
fmt.Println("Authorization header:", in.Headers["Authorization"])
fmt.Println("Pet:", in.Body)
fmt.Println("Pet's name:", in.Body.Name)
http.HandleFunc("/pets", fetch.ToHandlerFunc(func(req fetch.Request[Pet]) (*fetch.Empty, error) {
fmt.Println("Request context:", req.Context)
fmt.Println("Authorization header:", req.Headers["Authorization"])
fmt.Println("Pet:", req.Body)
fmt.Println("Pet's name:", req.Body.Name)
return nil, nil
}))
```
If you have go1.22 and above you can access the wildcards as well.
If you have go1.23 and above you can access the wildcards as well.
```go
http.HandleFunc("GET /pets/{id}", fetch.ToHandlerFunc(func(in fetch.Request[fetch.Empty]) (*fetch.Empty, error) {
fmt.Println("id from url:", in.PathValue("id"))
fmt.Println("id from url:", in.PathValues["id"])
return nil, nil
}))
```
```
To customize http attributes of the response, wrap the output with `fetch.Response`
```go
http.HandleFunc("/pets", fetch.ToHandlerFunc(func(_ fetch.Empty) (fetch.Response[*Pet], error) {
Expand Down
39 changes: 38 additions & 1 deletion to_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"io"
"net/http"
"reflect"
"runtime"
"strings"
)

var defaultHandlerConfig = HandlerConfig{
Expand Down Expand Up @@ -81,7 +83,8 @@ func ToHandlerFunc[In any, Out any](apply ApplyFunc[In, Out]) http.HandlerFunc {
}
}
valueOf := reflect.Indirect(reflect.ValueOf(&in))
valueOf.FieldByName("Request").Set(reflect.ValueOf(r))
valueOf.FieldByName("PathValues").Set(reflect.ValueOf(extractPathValues(r)))
valueOf.FieldByName("Context").Set(reflect.ValueOf(r.Context()))
valueOf.FieldByName("Headers").Set(reflect.ValueOf(uniqueHeaders(r.Header)))
valueOf.FieldByName("Body").Set(reflect.ValueOf(resInstance).Elem())
} else if !isEmptyType(in) {
Expand Down Expand Up @@ -115,3 +118,37 @@ func ToHandlerFunc[In any, Out any](apply ApplyFunc[In, Out]) http.HandlerFunc {
}
}
}

func extractPathValues(r *http.Request) map[string]string {
if !isGo23AndAbove() || r == nil {
return map[string]string{}
}

req := reflect.ValueOf(r)

parts := strings.Split(req.Elem().FieldByName("Pattern").String(), "/")
result := make(map[string]string)
for _, part := range parts {
if len(part) > 2 && strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") {
wildcard := part[1 : len(part)-1]
values := req.MethodByName("PathValue").Call([]reflect.Value{reflect.ValueOf(wildcard)})
if len(values) != 1 {
continue
}
if v := values[0].String(); v != "" {
result[wildcard] = v
}
}
}
return result
}

func isGo23AndAbove() bool {
if strings.HasPrefix(runtime.Version(), "go1.21") {
return false
}
if strings.HasPrefix(runtime.Version(), "go1.22") {
return false
}
return true
}
28 changes: 27 additions & 1 deletion to_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"net/http"
"testing"
_ "unsafe"
)

func TestToHandlerFunc_EmptyIn(t *testing.T) {
Expand Down Expand Up @@ -88,7 +89,7 @@ func TestToHandlerFunc_Header(t *testing.T) {

func TestToHandlerFunc_Context(t *testing.T) {
f := ToHandlerFunc(func(in Request[Empty]) (Empty, error) {
assert(t, in.Context().Err(), nil)
assert(t, in.Context.Err(), nil)
return Empty{}, nil
})
mw := newMockWriter()
Expand Down Expand Up @@ -153,3 +154,28 @@ func TestToHandlerFunc_Middleware(t *testing.T) {
mux.ServeHTTP(mw, r)
assert(t, mw.status, 422)
}

// to run it, update go.mod to 1.23
//func TestToHandlerFunc_ExtractPathValues(t *testing.T) {
// mw := newMockWriter()
// mux := http.NewServeMux()
// mux.HandleFunc("POST /categories/{category}/ids/{id}", func(w http.ResponseWriter, r *http.Request) {
// res := extractPathValues(r)
// if len(res) != 2 || res["category"] != "cats" || res["id"] != "1" {
// t.Errorf("extractPathValues(r) got: %+v", res)
// }
// w.WriteHeader(422)
// })
// r, err := http.NewRequest("POST", "/categories/cats/ids/1", bytes.NewBuffer([]byte(`{"name":"Charles"}`)))
// assert(t, err, nil)
// mux.ServeHTTP(mw, r)
// assert(t, mw.status, 422)
//}

func TestToHandlerFunc_ExtractPathValues_GoLess23(t *testing.T) {
if !isGo23AndAbove() {
if len(extractPathValues(&http.Request{})) != 0 {
t.Errorf("expect zero map")
}
}
}
13 changes: 9 additions & 4 deletions wrappers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package fetch

import (
"net/http"
"context"
"reflect"
"strings"
)
Expand Down Expand Up @@ -54,9 +54,14 @@ e.g.
}))
*/
type Request[T any] struct {
*http.Request
Headers map[string]string
Body T
Context context.Context
// Only available in go1.23 and above.
// PathValue was introduced in go1.22 but
// there was no reliable way to extract them.
// go1.23 introduced http.Request.Pattern allowing to list the wildcards.
PathValues map[string]string
Headers map[string]string
Body T
}

// Empty represents an empty response or request body, skipping JSON handling.
Expand Down

0 comments on commit 6b208d3

Please sign in to comment.