Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading