Skip to content

Commit

Permalink
Downgrade to go1.21, Request wrapper (#34)
Browse files Browse the repository at this point in the history
* Request wrapper

* ci, update go-version

* use http.Request within fetch.Request

* downgrade to go1.21

* more tests
  • Loading branch information
glossd authored Dec 8, 2024
1 parent 37195c4 commit 8735d26
Show file tree
Hide file tree
Showing 16 changed files with 336 additions and 306 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 ./...
46 changes: 29 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@


## Installing
This is a zero-dependency package. It requires Go version 1.22 or above.
This is a zero-dependency package. It requires Go version 1.21 or above.
```shell
go get github.com/glossd/fetch
```
Expand Down Expand Up @@ -108,9 +108,9 @@ if resp.Status == 200 {
fmt.Println("Response headers", resp.Headers)
}
```
If you don't need the HTTP body you can use `fetch.ResponseEmpty`
If you don't need the HTTP body you can use `fetch.Empty` or `fetch.Response[fetch.Empty]` to access http attributes
```go
res, err := fetch.Delete[fetch.ResponseEmpty]("https://petstore.swagger.io/v2/pet/10")
res, err := fetch.Delete[fetch.Response[fetch.Empty]]("https://petstore.swagger.io/v2/pet/10")
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -287,13 +287,13 @@ type Config struct {
```

## HTTP Handlers
`fetch.ToHandlerFunc` converts `func(in) (out, error)` signature function into `http.HandlerFunc`.
It unmarshals the HTTP request body into the function argument then marshals the returned value into the HTTP response body.
`fetch.ToHandlerFunc` converts `func(in) (out, error)` signature function into `http.HandlerFunc`. It does all the json and http handling for you.
The HTTP request body unmarshalls into the function argument. The return value is marshaled into the HTTP response body.
```go
type Pet struct {
Name string
}
http.HandleFunc("POST /pets", fetch.ToHandlerFunc(func(in Pet) (*Pet, error) {
http.HandleFunc("/pets", fetch.ToHandlerFunc(func(in Pet) (*Pet, error) {
if in.Name == "" {
return nil, fmt.Errorf("name can't be empty")
}
Expand All @@ -303,22 +303,34 @@ http.ListenAndServe(":8080", nil)
```
If you don't need request or response body, use `fetch.Empty` to fit the function signature.
```go
http.HandleFunc("GET /default-pet", fetch.ToHandlerFunc(func(_ fetch.Empty) (Pet, error) {
http.HandleFunc("/default-pet", fetch.ToHandlerFunc(func(_ fetch.Empty) (Pet, error) {
return Pet{Name: "Teddy"}, nil
}))
```
If you need to access path value or HTTP header use tags below:
If you need to access http request attributes wrap the input with `fetch.Request`. `http.Request` will be embedded to the input.
```go
type PetRequest struct {
Ctx context.Context // http.Request.Context() will be inserted into any field with context.Context type.
ID int `pathval:"id"` // {id} wildcard will be inserted into ID field.
Auth string `header:"Authorization"` // Authorization header will be inserted into Auth field.
Name string // untagged fields will be unmarshalled from the request body.
type Pet struct {
Name string
}
http.HandleFunc("GET /pets/{id}", fetch.ToHandlerFunc(func(in PetRequest) (fetch.Empty, error) {
fmt.Println("Pet's id from url:", in.ID)
fmt.Println("Authorization header:", in.Auth)
return fetch.Empty{}, nil
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)
return nil, nil
}))
```
If you have go1.22 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"))
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) {
return Response[*Pet]{Status: 201, Body: &Pet{Name: "Lola"}}, nil
}))
```
The error format can be customized with the `fetch.SetRespondErrorFormat` global setter.
Expand Down
24 changes: 11 additions & 13 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 Expand Up @@ -90,5 +88,5 @@ func jqerr(format string, a ...any) *JQError {
}

func IsJQError(v any) bool {
return reflect.TypeOf(v) == reflect.TypeFor[*JQError]()
return reflect.TypeOf(v) == reflectTypeFor[*JQError]()
}
23 changes: 10 additions & 13 deletions fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func Get[T any](url string, config ...Config) (T, error) {
config = []Config{{}}
}
config[0].Method = http.MethodGet
return Request[T](url, config...)
return Do[T](url, config...)
}

// GetJ is a wrapper for Get[fetch.J]
Expand Down Expand Up @@ -60,7 +60,7 @@ func requestWithBody[T any](url string, method string, body any, config ...Confi
return t, nonHttpErr("invalid body: ", err)
}
config[0].Body = b
return Request[T](url, config...)
return Do[T](url, config...)
}

func bodyToString(v any) (string, error) {
Expand All @@ -78,26 +78,26 @@ func Delete[T any](url string, config ...Config) (T, error) {
config = []Config{{}}
}
config[0].Method = http.MethodDelete
return Request[T](url, config...)
return Do[T](url, config...)
}

func Head[T any](url string, config ...Config) (T, error) {
if len(config) == 0 {
config = []Config{{}}
}
config[0].Method = http.MethodHead
return Request[T](url, config...)
return Do[T](url, config...)
}

func Options[T any](url string, config ...Config) (T, error) {
if len(config) == 0 {
config = []Config{{}}
}
config[0].Method = http.MethodOptions
return Request[T](url, config...)
return Do[T](url, config...)
}

func Request[T any](url string, config ...Config) (T, error) {
func Do[T any](url string, config ...Config) (T, error) {
var cfg Config
if len(config) > 0 {
cfg = config[0]
Expand Down Expand Up @@ -164,14 +164,13 @@ func Request[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 Request[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 Request[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
20 changes: 10 additions & 10 deletions fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func TestMain(m *testing.M) {
}

func TestRequestString(t *testing.T) {
res, err := Request[string]("my.ip")
res, err := Do[string]("my.ip")
if err != nil {
t.Fatal(err)
}
Expand All @@ -23,7 +23,7 @@ func TestRequestString(t *testing.T) {
}

func TestRequestBytes(t *testing.T) {
res, err := Request[[]byte]("array.int")
res, err := Do[[]byte]("array.int")
if err != nil {
t.Fatal(err)
}
Expand All @@ -34,7 +34,7 @@ func TestRequestBytes(t *testing.T) {
}

func TestRequestArray(t *testing.T) {
res, err := Request[[]int]("array.int")
res, err := Do[[]int]("array.int")
if err != nil {
t.Fatal(err)
}
Expand All @@ -44,7 +44,7 @@ func TestRequestArray(t *testing.T) {
}

func TestRequestAny(t *testing.T) {
res, err := Request[any]("key.value")
res, err := Do[any]("key.value")
if err != nil {
t.Fatal(err)
}
Expand All @@ -56,7 +56,7 @@ func TestRequestAny(t *testing.T) {
t.Errorf("map wasn't parsed")
}

res2, err := Request[any]("array.int")
res2, err := Do[any]("array.int")
if err != nil {
t.Fatal(err)
}
Expand All @@ -73,7 +73,7 @@ func TestRequest_ResponseT(t *testing.T) {
type TestStruct struct {
Key string
}
res, err := Request[Response[TestStruct]]("key.value")
res, err := Do[Response[TestStruct]]("key.value")
if err != nil {
t.Error(err)
}
Expand All @@ -89,7 +89,7 @@ func TestRequest_ResponseT(t *testing.T) {
t.Errorf("wrong body")
}

res2, err := Request[Response[string]]("my.ip")
res2, err := Do[Response[string]]("my.ip")
if err != nil {
t.Fatal(err)
}
Expand All @@ -99,7 +99,7 @@ func TestRequest_ResponseT(t *testing.T) {
}

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

_, err = Request[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")
}
}

func TestRequest_Error(t *testing.T) {
_, err := Request[string]("400.error")
_, err := Do[string]("400.error")
if err == nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/glossd/fetch

go 1.22
go 1.21
2 changes: 1 addition & 1 deletion j.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ func (n Nil) AsBoolean() (bool, bool) { return false, false }
func (n Nil) IsNil() bool { return true }

func isJNil(v any) bool {
return v == nil || reflect.TypeOf(v) == reflect.TypeFor[Nil]()
return v == nil || reflect.TypeOf(v) == reflectTypeFor[Nil]()
}

func nextSep(pattern string) (int, string) {
Expand Down
2 changes: 1 addition & 1 deletion parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func UnmarshalInto(j string, v any) error {

rve := rv.Elem()
var isAny = rve.Kind() == reflect.Interface && rve.NumMethod() == 0
if isAny || rve.Type() == reflect.TypeFor[J]() {
if isAny || rve.Type() == reflectTypeFor[J]() {
var a any
err := json.Unmarshal([]byte(j), &a)
if err != nil {
Expand Down
20 changes: 18 additions & 2 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,6 +55,17 @@ func Respond(w http.ResponseWriter, body any, config ...RespondConfig) error {
if cfg.ErrorStatus == 0 {
cfg.ErrorStatus = 500
}
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 @@ -73,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
Loading

0 comments on commit 8735d26

Please sign in to comment.