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
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
Loading