From 6b208d3d5044c5f82ad1eb3641929a6cc8c646c6 Mon Sep 17 00:00:00 2001 From: Dennis Gloss Date: Sun, 8 Dec 2024 19:03:33 +0100 Subject: [PATCH 1/2] Request wrapper remove http.Request --- README.md | 16 ++++++++-------- to_handler.go | 39 ++++++++++++++++++++++++++++++++++++++- to_handler_test.go | 28 +++++++++++++++++++++++++++- wrappers.go | 13 +++++++++---- 4 files changed, 82 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 566e92d..2e86aa6 100644 --- a/README.md +++ b/README.md @@ -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) { diff --git a/to_handler.go b/to_handler.go index 02c6a94..e392431 100644 --- a/to_handler.go +++ b/to_handler.go @@ -5,6 +5,8 @@ import ( "io" "net/http" "reflect" + "runtime" + "strings" ) var defaultHandlerConfig = HandlerConfig{ @@ -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) { @@ -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 +} diff --git a/to_handler_test.go b/to_handler_test.go index 92b0249..134b390 100644 --- a/to_handler_test.go +++ b/to_handler_test.go @@ -4,6 +4,7 @@ import ( "bytes" "net/http" "testing" + _ "unsafe" ) func TestToHandlerFunc_EmptyIn(t *testing.T) { @@ -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() @@ -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") + } + } +} diff --git a/wrappers.go b/wrappers.go index 2aed170..be9edff 100644 --- a/wrappers.go +++ b/wrappers.go @@ -1,7 +1,7 @@ package fetch import ( - "net/http" + "context" "reflect" "strings" ) @@ -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. From abec8c6dd9e1204e2e67edf98302f25297ac515a Mon Sep 17 00:00:00 2001 From: Dennis Gloss Date: Sun, 8 Dec 2024 19:04:36 +0100 Subject: [PATCH 2/2] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2e86aa6..1fff990 100644 --- a/README.md +++ b/README.md @@ -315,8 +315,8 @@ type Pet struct { 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) + fmt.Println("Pet:", req.Body) + fmt.Println("Pet's name:", req.Body.Name) return nil, nil })) ```