Skip to content

Commit

Permalink
Add As* methods to fetch.J
Browse files Browse the repository at this point in the history
  • Loading branch information
glossd committed Oct 20, 2024
1 parent d4452a5 commit 9c06542
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 30 deletions.
50 changes: 31 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,31 +187,43 @@ fmt.Println("Pet's name is", j.Q(".name"))
fmt.Println("Pet's category name is", j.Q(".category.name"))
fmt.Println("First tag's name is", j.Q(".tags[0].name"))
```
Depending on the JSON data type the queried `fetch.J` could be one of these types
Method `fetch.J#Q` returns `fetch.J`. You can use the method `Q` on the result as well.
```go
category := j.Q(".category")
fmt.Println("Pet's category object", category)
fmt.Println("Pet's category name is", category.Q(".name"))
```
To convert `fetch.J` to a basic value use one of `As*` methods

| Type | Go definition | JSON data type |
|-----------|-----------------|-------------------------------------|
| fetch.M | map[string]any | object |
| fetch.A | []any | array |
| fetch.F | float64 | number |
| fetch.S | string | string |
| fetch.B | bool | boolean |
| fetch.Nil | (nil) *struct{} | null, undefined, anything not found |

If you want `fetch.J` to return the value of the definition type, call method `fetch.J#Raw()`.
E.g. check if `fetch.J` is `nil`
| J Method | Return type |
|-----------|----------------|
| AsObject | map[string]any |
| AsArray | []any |
| AsNumber | float64 |
| AsString | string |
| AsBoolean | bool |
E.g.
```go
j, _ := fetch.Unmarshal[fetch.J]("{}")
if j.Q(".name").Raw() == nil {
// key 'name' doesn't exist
j, err := fetch.Unmarshal[fetch.J](`{"price": 14.99}`)
if err != nil {
panic(err)
}
n, ok := j.Q(".price").AsNumber()
if !ok {
// not a number
}
fmt.Printf("Price: %f\n", n) // n is a float64
```

Method `fetch.J#Q` returns `fetch.J`. You can use the method `Q` on the result as well.
`fetch.J` is only `nil` when an error happens. Use `IsNil` to check the value on presence.
```go
category := j.Q(".category")
fmt.Println("Pet's category object", category)
fmt.Println("Pet's category name is", category.Q(".name"))
j, err := fetch.Unmarshal[fetch.J]("{}")
if err != nil {
panic(err)
}
if j.Q(".price").IsNil() {
// key 'price' doesn't exist
}
```

### JSON handling
Expand Down
19 changes: 10 additions & 9 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func httpErr(prefix string, err error, r *http.Response) *Error {
return &Error{inner: err, Msg: prefix + err.Error(), Status: r.StatusCode, Headers: uniqueHeaders(r.Header)}
}

// JQError is returned from J.Q on invalid syntax.
// It seems to be better to return this than panic.
type JQError struct {
s string
}
Expand All @@ -57,12 +59,7 @@ func (e *JQError) Unwrap() error {
return errors.New(e.s)
}

func (e *JQError) Q(pattern string) J {
if e == nil {
return S("")
}
return S(e.Error())
}
func (e *JQError) Q(pattern string) J { return e }

func (e *JQError) String() string {
if e == nil {
Expand All @@ -71,9 +68,13 @@ func (e *JQError) String() string {
return e.Error()
}

func (e *JQError) Raw() any {
return e
}
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...)}
Expand Down
74 changes: 73 additions & 1 deletion j.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@ package fetch

import (
"fmt"
"reflect"
"strconv"
"strings"
)

var jnil Nil

// J represents arbitrary JSON.
// Depending on the JSON data type the queried `fetch.J` could be one of these types
//
// | Type | Go definition | JSON data type |
// |-----------|-----------------|-------------------------------------|
// | fetch.M | map[string]any | object |
// | fetch.A | []any | array |
// | fetch.F | float64 | number |
// | fetch.S | string | string |
// | 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.
Expand Down Expand Up @@ -39,6 +51,19 @@ type J interface {
// B -> bool
// Nil -> nil
Raw() any

// AsObject is a convenient type assertion if the underlying value holds 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() ([]any, bool)
// AsNumber is a convenient type assertion if the underlying value holds a float64.
AsNumber() (float64, bool)
// AsString is a convenient type assertion if the underlying value holds a string.
AsString() (string, bool)
// AsBoolean is a convenient type assertion if the underlying value holds a bool.
AsBoolean() (bool, bool)
// IsNil check if the underlying value is fetch.Nil
IsNil() bool
}

// M represents a JSON object.
Expand Down Expand Up @@ -74,6 +99,13 @@ func (m M) Raw() any {
return map[string]any(m)
}

func (m M) AsObject() (map[string]any, bool) { return m, true }
func (m M) AsArray() ([]any, bool) { return nil, false }
func (m M) AsNumber() (float64, bool) { return 0, false }
func (m M) AsString() (string, bool) { return "", false }
func (m M) AsBoolean() (bool, bool) { return false, false }
func (m M) IsNil() bool { return false }

// A represents a JSON array
type A []any

Expand Down Expand Up @@ -124,6 +156,13 @@ func (a A) Raw() any {
return []any(a)
}

func (a A) AsObject() (map[string]any, bool) { return nil, false }
func (a A) AsArray() ([]any, bool) { return a, true }
func (a A) AsNumber() (float64, bool) { return 0, false }
func (a A) AsString() (string, bool) { return "", false }
func (a A) AsBoolean() (bool, bool) { return false, false }
func (a A) IsNil() bool { return false }

func parseValue(v any, remaining string, sep string) J {
if j, ok := v.(J); ok {
return j.Q(remaining)
Expand Down Expand Up @@ -164,6 +203,13 @@ func (f F) Raw() any {
return float64(f)
}

func (f F) AsObject() (map[string]any, bool) { return nil, false }
func (f F) AsArray() ([]any, bool) { return nil, false }
func (f F) AsNumber() (float64, bool) { return float64(f), true }
func (f F) AsString() (string, bool) { return "", false }
func (f F) AsBoolean() (bool, bool) { return false, false }
func (f F) IsNil() bool { return false }

// S can't be a root value.
type S string

Expand All @@ -185,6 +231,13 @@ func (s S) Raw() any {
return string(s)
}

func (s S) AsObject() (map[string]any, bool) { return nil, false }
func (s S) AsArray() ([]any, bool) { return nil, false }
func (s S) AsNumber() (float64, bool) { return 0, false }
func (s S) AsString() (string, bool) { return string(s), true }
func (s S) AsBoolean() (bool, bool) { return false, false }
func (s S) IsNil() bool { return false }

// B represents a JSON boolean
type B bool

Expand All @@ -206,9 +259,16 @@ func (b B) Raw() any {
return bool(b)
}

func (b B) AsObject() (map[string]any, bool) { return nil, false }
func (b B) AsArray() ([]any, bool) { return nil, false }
func (b B) AsNumber() (float64, bool) { return 0, false }
func (b B) AsString() (string, bool) { return "", false }
func (b B) AsBoolean() (bool, bool) { return bool(b), true }
func (b B) IsNil() bool { return false }

type nilStruct struct{}

// Nil represents any not found value. The pointer's value is always nil.
// Nil represents any not found value. The pointer's value is always nil.
// It exists to prevent nil pointer dereference when retrieving Raw value.
// Nil can't be a root value.
type Nil = *nilStruct
Expand All @@ -225,6 +285,17 @@ func (n Nil) Raw() any {
return nil
}

func (n Nil) AsObject() (map[string]any, bool) { return nil, false }
func (n Nil) AsArray() ([]any, bool) { return nil, false }
func (n Nil) AsNumber() (float64, bool) { return 0, false }
func (n Nil) AsString() (string, bool) { return "", false }
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) == typeFor[Nil]()
}

func nextSep(pattern string) (int, string) {
dot := strings.Index(pattern, ".")
bracket := strings.Index(pattern, "[")
Expand Down Expand Up @@ -276,6 +347,7 @@ func convert(v any) J {
func marshalJ(v any) string {
r, err := Marshal(v)
if err != nil {
// shouldn't happen, A and M are marshalable.
return err.Error()
}
return r
Expand Down
89 changes: 88 additions & 1 deletion j_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ func TestJ_Nil(t *testing.T) {
if j.Q(".id") == nil {
t.Errorf("didn't expect J value to be nil")
}
fmt.Println(j.Q(".id"))
if !j.Q("id").IsNil() {
t.Errorf("expected J.IsNil to be true")
}
if j.Q(".id").Raw() != nil {
t.Errorf("expected id to be nil")
}
Expand All @@ -140,3 +142,88 @@ func TestJ_Nil(t *testing.T) {
t.Errorf("expected id to be nil")
}
}

func TestJ_AsFirstValue(t *testing.T) {
m, _ := mustUnmarshal(`{"key":"value"}`).AsObject()
if m["key"] != "value" {
t.Errorf("object value is wrong")
}

a, _ := mustUnmarshal(`[1, 2, 3]`).AsArray()
if len(a) != 3 {
t.Errorf("array value is wrong")
}

n, _ := mustUnmarshal(`1`).AsNumber()
if n != 1 {
t.Errorf("number value is wrong")
}

s, _ := mustUnmarshal(`{"key":"value"}`).Q("key").AsString()
if s != "value" {
t.Errorf("string value is wrong")
}

b, _ := mustUnmarshal(`true`).AsBoolean()
if !b {
t.Errorf("boolean value is wrong")
}
}

type AsCheck struct {
Boolean bool
Number bool
String bool
Array bool
Object bool
IsNil bool
}

func TestJ_AsSecondValue(t *testing.T) {

type testCase struct {
I J
O AsCheck
}

var cases = []testCase{
{I: mustUnmarshal(`{"key":"value"}`), O: AsCheck{Object: true}},
{I: mustUnmarshal(`{"outer":{"key":"value"}}`).Q("outer"), O: AsCheck{Object: true}},
{I: mustUnmarshal(`{"key":[1, 2, 3]}`).Q("key"), O: AsCheck{Array: true}},
{I: mustUnmarshal(`[1, 2]`), O: AsCheck{Array: true}},
{I: mustUnmarshal(`0`), O: AsCheck{Number: true}},
{I: mustUnmarshal(`1`), O: AsCheck{Number: true}},
{I: mustUnmarshal(`false`), O: AsCheck{Boolean: true}},
{I: mustUnmarshal(`true`), O: AsCheck{Boolean: true}},
{I: mustUnmarshal(`{"key":"value"}`).Q("key"), O: AsCheck{String: true}},
{I: mustUnmarshal(`{}`).Q("key"), O: AsCheck{IsNil: true}},
}
for i, c := range cases {
if c.I.IsNil() != c.O.IsNil {
t.Errorf("%d nil mismatch", i)
}
if _, ok := c.I.AsBoolean(); ok != c.O.Boolean {
t.Errorf("%d boolean mismatch", i)
}
if _, ok := c.I.AsNumber(); ok != c.O.Number {
t.Errorf("%d number mismatch", i)
}
if _, ok := c.I.AsString(); ok != c.O.String {
t.Errorf("%d string mismatch", i)
}
if _, ok := c.I.AsArray(); ok != c.O.Array {
t.Errorf("%d array mismatch", i)
}
if _, ok := c.I.AsObject(); ok != c.O.Object {
t.Errorf("%d object mismatch", i)
}
}
}

func mustUnmarshal(s string) J {
j, err := Unmarshal[J](s)
if err != nil {
panic(err)
}
return j
}
13 changes: 13 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ import (
// return t
//}

// UnmarshalJ sends J.String() to Unmarshal.
func UnmarshalJ[T any](j J) (T, error) {
if isJNil(j) {
var t T
return t, fmt.Errorf("cannot unmarshal nil J")
}
if IsJQError(j) {
var t T
return t, fmt.Errorf("cannot unmarshal JQerror")
}
return Unmarshal[T](j.String())
}

// Unmarshal is a generic wrapper for UnmarshalInto
func Unmarshal[T any](j string) (T, error) {
var t T
Expand Down
16 changes: 16 additions & 0 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,19 @@ func TestUnmarshalString(t *testing.T) {
t.Fatal(err)
}
}

func TestUnmarshalJ(t *testing.T) {
var n Nil
_, err := UnmarshalJ[string](n)
if err == nil {
t.Fatalf("nil shouldn't be unmarshaled")
}

r, err := UnmarshalJ[map[string]string](M{"name": "Lola"})
if err != nil {
t.Fatal("UnmarshalJ error:", err)
}
if r["name"] != "Lola" {
t.Errorf("UnmarshalJ result mismatch, got=%s", r)
}
}

0 comments on commit 9c06542

Please sign in to comment.