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
20 changes: 12 additions & 8 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.21 or above.
This is a zero-dependency package. It requires Go version **1.21** or above. **Stable** version is out!
```shell
go get github.com/glossd/fetch
```
Expand Down Expand Up @@ -293,20 +293,24 @@ type Config struct {
```

## HTTP Handlers
`fetch.ToHandlerFunc` converts `func(in) (out, error)` signature function into `http.HandlerFunc`. It does all the json and http handling for you.
`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("/pets", fetch.ToHandlerFunc(func(in Pet) (*Pet, error) {
http.HandleFunc("/pets/update", fetch.ToHandlerFunc(func(in Pet) (*Pet, error) {
if in.Name == "" {
return nil, fmt.Errorf("name can't be empty")
}
return &Pet{Name: in.Name}, nil
return &Pet{Name: in.Name + " 3000"}, nil
}))
http.ListenAndServe(":8080", nil)
```
```shell
$ curl localhost:8080/pets/update -d '{"name":"Lola"}'
{"name":"Lola 3000"}
```
If you have empty request or response body or you want to ignore them, use `fetch.Empty`:
```go
http.HandleFunc("/default-pet", fetch.ToHandlerFunc(func(_ fetch.Empty) (Pet, error) {
Expand Down Expand Up @@ -339,16 +343,16 @@ http.HandleFunc("/pets", fetch.ToHandlerFunc(func(_ fetch.Empty) (fetch.Response
return Response[*Pet]{Status: 201, Body: &Pet{Name: "Lola"}}, nil
}))
```
The error format can be customized with the `fetch.SetRespondErrorFormat` global setter.
To log http errors with your logger call `SetDefaultHandlerConfig`
The error format can be customized with the `fetch.SetHandlerErrorFormat` global setter.
To log `ToHandleFunc` errors with your logger call `SetHandlerConfig`
```go
fetch.SetDefaultHandlerConfig(fetch.HandlerConfig{ErrorHook: func(err error) {
fetch.SetHandlerConfig(fetch.HandlerConfig{ErrorHook: func(err error) {
mylogger.Errorf("fetch http error: %s", err)
}})
```
To add middleware before handling request in `fetch.ToHandlerFunc`
```go
fetch.SetDefaultHandlerConfig(fetch.HandlerConfig{Middleware: func(w http.ResponseWriter, r *http.Request) bool {
fetch.SetHandlerConfig(fetch.HandlerConfig{Middleware: func(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("Authorization") == "" {
w.WriteHeader(401)
return true
Expand Down
48 changes: 0 additions & 48 deletions error.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package fetch

import (
"errors"
"fmt"
"net/http"
"reflect"
)

type Error struct {
Expand Down Expand Up @@ -45,48 +42,3 @@ func httpErr(prefix string, err error, r *http.Response, body []byte) *Error {
Body: string(body),
}
}

// JQError is returned from J.Q on invalid syntax.
// It seems to be better to return this than panic.
type JQError struct {
s string
}

func (e *JQError) Error() string {
if e == nil {
return ""
}
return fmt.Sprintf("JQError: %s", e.s)
}

func (e *JQError) Unwrap() error {
if e == nil {
return nil
}
return errors.New(e.s)
}

func (e *JQError) Q(pattern string) J { return e }

func (e *JQError) String() string {
if e == nil {
return ""
}
return e.Error()
}

func (e *JQError) Raw() any { return e }
func (e *JQError) AsObject() (map[string]any, bool) { return nil, false }
func (e *JQError) AsArray() ([]any, bool) { return nil, false }
func (e *JQError) AsNumber() (float64, bool) { return 0, false }
func (e *JQError) AsString() (string, bool) { return "", false }
func (e *JQError) AsBoolean() (bool, bool) { return false, false }
func (e *JQError) IsNil() bool { return false }

func jqerr(format string, a ...any) *JQError {
return &JQError{s: fmt.Sprintf(format, a...)}
}

func IsJQError(v any) bool {
return reflect.TypeOf(v) == reflectTypeFor[*JQError]()
}
19 changes: 0 additions & 19 deletions error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,3 @@ func TestError_Format(t *testing.T) {
t.Errorf("error failed, got: %s", err)
}
}

func TestIsJQError(t *testing.T) {
f := func() any {
return jqerr("invalid")
}

got := f()
if !IsJQError(got) {
t.Errorf("supposed to be JQError")
}

if IsJQError(errors.New("hello")) {
t.Errorf("not supposed to be JQError")
}

if fmt.Sprint(jqerr("hello")) != "JQError: hello" {
t.Errorf("unexpected format")
}
}
5 changes: 0 additions & 5 deletions fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@ func Get[T any](url string, config ...Config) (T, error) {
return Do[T](url, config...)
}

// GetJ is a wrapper for Get[fetch.J]
func GetJ(url string, config ...Config) (J, error) {
return Get[J](url, config...)
}

func Post[T any](url string, body any, config ...Config) (T, error) {
return requestWithBody[T](url, http.MethodPost, body, config...)
}
Expand Down
78 changes: 42 additions & 36 deletions j.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,48 +19,50 @@ import (
// | fetch.B | bool | boolean |
// | fetch.Nil | (nil) *struct{} | null, undefined, anything not found |
type J interface {
// Q parses jq-like patterns and returns according to the path value.
// E.g.
//{
// "name": "Jason",
// "category": {
// "name":"dogs"
// }
// "tags": [{"name":"briard"}]
//}
//
// Whole json: fmt.Println(j) or fmt.Println(j.Q("."))
// Retrieve name: j.Q(".name")
// Retrieve category name: j.Q(".category.name")
// Retrieve first tag's name: j.Q(".tags[0].name")
// If the value wasn't found, instead of nil value it will return Nil.
// if the pattern syntax is invalid, it returns JQError.
/*
Q parses jq-like patterns and returns according to the path value.
If the value wasn't found or syntax is incorrect, Q will return Nil.
Examples:

j := fetch.Parse(`{
"name": "Jason",
"category": {
"name":"dogs"
}
"tags": [{"name":"briard"}]
}`)

Print json: fmt.Println(j)
Retrieve name: j.Q(".name")
Retrieve category name: j.Q(".category.name")
Retrieve first tag's name: j.Q(".tags[0].name")
*/
Q(pattern string) J

// String returns JSON formatted string.
String() string

// Raw converts the value to its definition and returns it.
// Elem converts J to its definition and returns it.
// Type-Definitions:
// M -> map[string]any
// A -> []any
// F -> float64
// S -> string
// B -> bool
// Nil -> nil
Raw() any
Elem() any

// AsObject is a convenient type assertion if the underlying value holds a map[string]any.
// AsObject is a convenient type assertion if J is a map[string]any.
AsObject() (map[string]any, bool)
// AsArray is a convenient type assertion if the underlying value holds a slice of type []any.
// AsArray is a convenient type assertion if J is a slice of type []any.
AsArray() ([]any, bool)
// AsNumber is a convenient type assertion if the underlying value holds a float64.
// AsNumber is a convenient type assertion if J is a float64.
AsNumber() (float64, bool)
// AsString is a convenient type assertion if the underlying value holds a string.
// AsString is a convenient type assertion if J is a string.
AsString() (string, bool)
// AsBoolean is a convenient type assertion if the underlying value holds a bool.
// AsBoolean is a convenient type assertion if J is a bool.
AsBoolean() (bool, bool)
// IsNil check if the underlying value is fetch.Nil
// IsNil check if J is fetch.Nil
IsNil() bool
}

Expand Down Expand Up @@ -93,7 +95,7 @@ func (m M) String() string {
return marshalJ(m)
}

func (m M) Raw() any {
func (m M) Elem() any {
return map[string]any(m)
}

Expand Down Expand Up @@ -121,12 +123,14 @@ func (a A) Q(pattern string) J {
}
closeBracket := strings.Index(pattern, "]")
if closeBracket == -1 {
return jqerr("expected ] for array index")
// expected ] for array index
return jnil
}
indexStr := pattern[1:closeBracket]
index, err := strconv.Atoi(indexStr)
if err != nil {
return jqerr("expected a number for array index, got: '%s'", indexStr)
// expected a number for array index
return jnil
}
if index < 0 || index >= len(a) {
// index out of range
Expand All @@ -140,7 +144,8 @@ func (a A) Q(pattern string) J {
}
i, sep := nextSep(remaining)
if i != 0 {
return jqerr("expected . or [, got: '%s'", beforeSep(remaining))
// expected . or [
return jnil
}

return parseValue(v, remaining, sep)
Expand All @@ -150,7 +155,7 @@ func (a A) String() string {
return marshalJ(a)
}

func (a A) Raw() any {
func (a A) Elem() any {
return []any(a)
}

Expand Down Expand Up @@ -197,7 +202,7 @@ func (f F) String() string {
return strconv.FormatFloat(float64(f), 'f', -1, 64)
}

func (f F) Raw() any {
func (f F) Elem() any {
return float64(f)
}

Expand Down Expand Up @@ -225,7 +230,7 @@ func (s S) String() string {
return string(s)
}

func (s S) Raw() any {
func (s S) Elem() any {
return string(s)
}

Expand Down Expand Up @@ -253,7 +258,7 @@ func (b B) String() string {
return strconv.FormatBool(bool(b))
}

func (b B) Raw() any {
func (b B) Elem() any {
return bool(b)
}

Expand All @@ -266,11 +271,12 @@ func (b B) IsNil() bool { return false }

type nilStruct struct{}

// Nil represents any not found or null values. The pointer's value is always nil.
// Nil represents any not found or null values. It is also used for syntax errors.
// Nil is pointer which value is always nil.
// However, when returned from any method, it doesn't equal nil, because
// a Go interface is not nil when it has a type.
// It exists to prevent nil pointer dereference when retrieving Raw value.
// It can be the root in J tree, because null alone is a valid JSON.
// It exists to prevent nil pointer dereference when retrieving Elem value.
// It can be the root of J tree, because null alone is a valid JSON.
type Nil = *nilStruct

// the single instance of Nil.
Expand All @@ -284,7 +290,7 @@ func (n Nil) String() string {
return "nil"
}

func (n Nil) Raw() any {
func (n Nil) Elem() any {
return nil
}

Expand Down
39 changes: 8 additions & 31 deletions j_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ func TestJ_Q(t *testing.T) {
{In: `[1, 2]`, P: ".[3]", E: nil},
{In: `{"tags":[{"id":12}]}`, P: ".tags.id", E: nil},
{In: `{"category":{"name":"dog"}}`, P: ".category[0]", E: nil},
{In: `[1, 2, 3]`, P: ".[1", E: nil},
{In: `[1, 2, 3]`, P: ".]1", E: nil},
{In: `[1, 2, 3]`, P: ".[]", E: nil},
{In: `[1, 2, 3]`, P: ".[hello]", E: nil},
{In: `[1, 2, 3]`, P: ".[0]name", E: nil},
}
for i, c := range cases {
j, err := Unmarshal[J](c.In)
Expand All @@ -54,39 +59,11 @@ func TestJ_Q(t *testing.T) {
// fmt.Print()
//}

got := j.Q(c.P).Raw()
got := j.Q(c.P).Elem()
if got != c.E {
t.Errorf("case #%d: wrong value, expected=%v, got=%v", i, c.E, got)
}
}

errorCases := []testCase{
{In: `[1, 2, 3]`, P: ".[1", E: "expected ] for array index"},
{In: `[1, 2, 3]`, P: ".[hello]", E: "expected a number for array index, got: 'hello'"},
{In: `[1, 2, 3]`, P: ".[0]name", E: "expected . or [, got: 'name'"},
}

for i, c := range errorCases {
j, err := Unmarshal[J](c.In)
if err != nil {
t.Errorf("Unmarshal error: %s", err)
continue
}

got := j.Q(c.P)
errStr, ok := c.E.(string)
if !ok {
panic("E should be string")
}
jqErr, ok := got.(*JQError)
if !ok {
t.Errorf("error case #%d: expected error, got=%v", i, got)
continue
}
if jqErr.s != errStr {
t.Errorf("error case #%d: wrong value, expected=%v, got=%v", i, errStr, jqErr.s)
}
}
}

func TestJ_String(t *testing.T) {
Expand Down Expand Up @@ -133,13 +110,13 @@ func TestJ_Nil(t *testing.T) {
if !j.Q("id").IsNil() {
t.Errorf("expected J.IsNil to be true")
}
if j.Q(".id").Raw() != nil {
if j.Q(".id").Elem() != nil {
t.Errorf("expected id to be nil")
}
if j.Q(".id").String() != "nil" {
t.Errorf("expected id to print nil")
}
if j.Q(".id").Q(".yaid").Raw() != nil {
if j.Q(".id").Q(".yaid").Elem() != nil {
t.Errorf("expected id to be nil")
}
}
Expand Down
Loading
Loading