Skip to content

Commit

Permalink
use http.Request within fetch.Request
Browse files Browse the repository at this point in the history
  • Loading branch information
glossd committed Dec 7, 2024
1 parent fe95e72 commit 47e7585
Show file tree
Hide file tree
Showing 13 changed files with 104 additions and 102 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ cmd/
tmp*
# Develop tools
.idea/
.vscode/
Makefile
.vscode/
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@


test21:
docker run --rm -v $$PWD:/usr/src/myapp -w /usr/src/myapp golang:1.21 go test ./...
test22:
docker run --rm -v $$PWD:/usr/src/myapp -w /usr/src/myapp golang:1.22 go test ./...
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,8 @@ type Pet struct {
Name string
}
http.HandleFunc("POST /pets/{id}", fetch.ToHandlerFunc(func(in fetch.Request[Pet]) (fetch.Empty, error) {
fmt.Println("Pet's id from url:", in.PathValues["id"])
fmt.Println("Request context:", in.Context)
fmt.Println("Pet's id from url:", in.PathValue("id"))
fmt.Println("Request context:", in.Context())
fmt.Println("Authorization header:", in.Headers["Authorization"])
fmt.Println("Pet:", in.Body)
return fetch.Empty{}, nil
Expand Down
22 changes: 10 additions & 12 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import (
)

type Error struct {
inner error
Msg string
Status int
Headers map[string]string
DuplicateHeaders map[string][]string
Body string
inner error
Msg string
Status int
Headers map[string]string
Body string
}

func (e *Error) Error() string {
Expand All @@ -39,12 +38,11 @@ func httpErr(prefix string, err error, r *http.Response, body []byte) *Error {
return nonHttpErr(prefix, err)
}
return &Error{
inner: err,
Msg: prefix + err.Error(),
Status: r.StatusCode,
Headers: uniqueHeaders(r.Header),
DuplicateHeaders: r.Header,
Body: string(body),
inner: err,
Msg: prefix + err.Error(),
Status: r.StatusCode,
Headers: uniqueHeaders(r.Header),
Body: string(body),
}
}

Expand Down
22 changes: 15 additions & 7 deletions fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,13 @@ func Do[T any](url string, config ...Config) (T, error) {
var t T
typeOf := reflect.TypeOf(t)

if typeOf != nil && typeOf == reflect.TypeFor[Empty]() && firstDigit(res.StatusCode) == 2 {
if isEmptyType(t) && firstDigit(res.StatusCode) == 2 {
return t, nil
}
if typeOf != nil && typeOf == reflect.TypeFor[ResponseEmpty]() && firstDigit(res.StatusCode) == 2 {
re := any(&t).(*ResponseEmpty)
if isResponseWithEmpty(t) && firstDigit(res.StatusCode) == 2 {
re := any(&t).(*Response[Empty])
re.Status = res.StatusCode
re.Headers = uniqueHeaders(res.Header)
re.DuplicateHeaders = res.Header
return t, nil
}

Expand All @@ -184,7 +183,7 @@ func Do[T any](url string, config ...Config) (T, error) {
return t, httpErr(fmt.Sprintf("http status=%d, body=", res.StatusCode), errors.New(string(body)), res, body)
}

if typeOf != nil && typeOf.PkgPath() == "github.com/glossd/fetch" && strings.HasPrefix(typeOf.Name(), "Response[") {
if isResponseWrapper(t) {
resType, ok := typeOf.FieldByName("Body")
if !ok {
panic("field Body is not found in Response")
Expand All @@ -199,9 +198,7 @@ func Do[T any](url string, config ...Config) (T, error) {

valueOf := reflect.Indirect(reflect.ValueOf(&t))
valueOf.FieldByName("Status").SetInt(int64(res.StatusCode))
valueOf.FieldByName("DuplicateHeaders").Set(reflect.ValueOf(res.Header))
valueOf.FieldByName("Headers").Set(reflect.ValueOf(uniqueHeaders(res.Header)))
valueOf.FieldByName("BodyBytes").SetBytes(body)
valueOf.FieldByName("Body").Set(reflect.ValueOf(resInstance).Elem())

return t, nil
Expand Down Expand Up @@ -257,6 +254,17 @@ func firstDigit(n int) int {
return i
}

func isResponseWrapper(v any) bool {
if v == nil {
return false
}
typeOf := reflect.TypeOf(v)
return typeOf.PkgPath() == "github.com/glossd/fetch" && strings.HasPrefix(typeOf.Name(), "Response[")
}
func isResponseWithEmpty(v any) bool {
return reflect.TypeOf(v) == reflect.TypeFor[Response[Empty]]()
}

func hasContentType(c Config) bool {
for k := range c.Headers {
if strings.ToLower(k) == "content-type" {
Expand Down
4 changes: 2 additions & 2 deletions fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func TestRequest_ResponseT(t *testing.T) {
}

func TestRequest_ResponseEmpty(t *testing.T) {
res, err := Do[ResponseEmpty]("key.value")
res, err := Do[Response[Empty]]("key.value")
if err != nil {
t.Error(err)
}
Expand All @@ -110,7 +110,7 @@ func TestRequest_ResponseEmpty(t *testing.T) {
t.Errorf("wrong headers")
}

_, err = Do[ResponseEmpty]("400.error")
_, err = Do[Response[Empty]]("400.error")
if err == nil || err.(*Error).Body != "Bad Request" {
t.Errorf("Even with ResponseEmpty error should read the body")
}
Expand Down
14 changes: 0 additions & 14 deletions request.go

This file was deleted.

21 changes: 18 additions & 3 deletions respond.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fetch
import (
"fmt"
"net/http"
"reflect"
"strings"
)

Expand Down Expand Up @@ -54,7 +55,17 @@ func Respond(w http.ResponseWriter, body any, config ...RespondConfig) error {
if cfg.ErrorStatus == 0 {
cfg.ErrorStatus = 500
}
// todo handle ResponseEmpty, Response
if isResponseWrapper(body) {
wrapper := reflect.ValueOf(body)
status := wrapper.FieldByName("Status").Int()
cfg.Status = int(status)
mapRange := wrapper.FieldByName("Headers").MapRange()
headers := make(map[string]string)
for mapRange.Next() {
headers[mapRange.Key().String()] = mapRange.Value().String()
}
cfg.Headers = headers
}
var err error
if !isValidHTTPStatus(cfg.Status) {
err := fmt.Errorf("RespondConfig.Status is invalid")
Expand All @@ -74,14 +85,18 @@ func Respond(w http.ResponseWriter, body any, config ...RespondConfig) error {
bodyStr = u
case []byte:
bodyStr = string(u)
case Empty, *Empty:
case Empty, *Empty, Response[Empty]:
bodyStr = ""
default:
isString = false
}
}
if !isString {
bodyStr, err = Marshal(body)
if isResponseWrapper(body) {
bodyStr, err = Marshal(reflect.ValueOf(body).FieldByName("Body").Interface())
} else {
bodyStr, err = Marshal(body)
}
if err != nil {
_ = respond(w, cfg.ErrorStatus, fmt.Sprintf(respondErrorFormat, err), isRespondErrorFormatJSON, cfg)
return fmt.Errorf("failed to marshal response body: %s", err)
Expand Down
21 changes: 21 additions & 0 deletions respond_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,27 @@ func TestSetRespondErrorFormat_InvalidFormats(t *testing.T) {
})
}

func TestRespondResponseEmpty(t *testing.T) {
mw := newMockWriter()
err := Respond(mw, Response[Empty]{Status: 204})
assert(t, err, nil)
if mw.status != 204 || len(mw.body) > 0 {
t.Errorf("wrong writer: %+v", mw)
}
}

func TestRespondResponse(t *testing.T) {
type Pet struct {
Name string
}
mw := newMockWriter()
err := Respond(mw, Response[Pet]{Status: 201, Body: Pet{Name: "Lola"}})
assert(t, err, nil)
if mw.status != 201 || string(mw.body) != `{"name":"Lola"}` {
t.Errorf("wrong writer: %+v, %s", mw, string(mw.body))
}
}

func TestRespondError(t *testing.T) {
mw := newMockWriter()
err := RespondError(mw, 400, fmt.Errorf("wrong"))
Expand Down
25 changes: 1 addition & 24 deletions to_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@ import (
"strings"
)

type handleTag = string

const (
pathvalTag handleTag = "pathval"
headerTag handleTag = "header"
)

var defaultHandlerConfig = HandlerConfig{
ErrorHook: func(err error) {
fmt.Printf("fetch.Handle failed to respond: %s\n", err)
Expand Down Expand Up @@ -89,8 +82,7 @@ func ToHandlerFunc[In any, Out any](apply ApplyFunc[In, Out]) http.HandlerFunc {
}
}
valueOf := reflect.Indirect(reflect.ValueOf(&in))
valueOf.FieldByName("Context").Set(reflect.ValueOf(r.Context()))
valueOf.FieldByName("PathValues").Set(reflect.ValueOf(extractPathValues(r)))
valueOf.FieldByName("Request").Set(reflect.ValueOf(r))
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 @@ -121,21 +113,6 @@ func ToHandlerFunc[In any, Out any](apply ApplyFunc[In, Out]) http.HandlerFunc {
}
}

func extractPathValues(r *http.Request) map[string]string {
parts := strings.Split(r.Pattern, "/")
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]
v := r.PathValue(wildcard)
if v != "" {
result[wildcard] = v
}
}
}
return result
}

func isStructType(v any) (reflect.Type, bool) {
typeOf := reflect.TypeOf(v)
if v == nil {
Expand Down
2 changes: 1 addition & 1 deletion to_handler_it_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestToHandlerFunc(t *testing.T) {

mux := http.NewServeMux()
mux.HandleFunc("/pets/{id}", ToHandlerFunc(func(in Request[Pet]) (*Pet, error) {
if in.PathValues["id"] != "1" {
if in.PathValue("id") != "1" {
t.Errorf("expected path value")
}
if in.Body.Name != "Lola" {
Expand Down
20 changes: 2 additions & 18 deletions to_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestToHandlerFunc_MultiplePathValue(t *testing.T) {
Name string
}
f := ToHandlerFunc(func(in Request[Pet]) (Empty, error) {
if in.PathValues["category"] != "cats" || in.PathValues["id"] != "1" {
if in.PathValue("category") != "cats" || in.PathValue("id") != "1" {
t.Errorf("wrong path value, got %v", in)
}
return Empty{}, nil
Expand All @@ -52,22 +52,6 @@ func TestToHandlerFunc_MultiplePathValue(t *testing.T) {
assert(t, mw.status, 200)
}

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_J(t *testing.T) {
f := ToHandlerFunc(func(in J) (J, error) {
assert(t, in.Q("name").String(), "Lola")
Expand Down Expand Up @@ -102,7 +86,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
42 changes: 25 additions & 17 deletions response.go → wrappers.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package fetch

import "net/http"

/*
Response is a wrapper type for (generic) ReturnType to be used in
the HTTP methods. It allows you to access HTTP attributes
Expand All @@ -18,22 +20,9 @@ e.g.
fmt.Println(res.Body.FirstName)
*/
type Response[T any] struct {
Status int
// HTTP headers are not unique.
// In the majority of the cases Headers is enough.
// Headers are filled with the last value from DuplicateHeaders.
DuplicateHeaders map[string][]string
Headers map[string]string
Body T
BodyBytes []byte
}

// ResponseEmpty is a special ResponseType that completely ignores the HTTP body.
// Can be used as the (generic) ReturnType for any HTTP method.
type ResponseEmpty struct {
Status int
Headers map[string]string
DuplicateHeaders map[string][]string
Status int
Headers map[string]string
Body T
}

func uniqueHeaders(headers map[string][]string) map[string]string {
Expand All @@ -47,6 +36,25 @@ func uniqueHeaders(headers map[string][]string) map[string]string {
return h
}

/*
Request can be used in ApplyFunc as a wrapper
for the input entity to access http attributes.
e.g.
type Pet struct {
Name string
}
http.HandleFunc("POST /pets/{id}", fetch.ToHandlerFunc(func(in fetch.Request[Pet]) (fetch.Empty, error) {
in.Context()
return fetch.Empty{}, nil
}))
*/
type Request[T any] struct {
*http.Request
Headers map[string]string
Body T
}

// Empty represents an empty response or request body, skipping JSON handling.
// Can be used in any HTTP method or to fit the signature of ApplyFunc.
// Can be used with the wrappers Response and Request or to fit the signature of ApplyFunc.
type Empty struct{}

0 comments on commit 47e7585

Please sign in to comment.