From 9c0654223abe721fb6ba76f47f6ebbc12dd61514 Mon Sep 17 00:00:00 2001 From: Dennis Gloss Date: Sun, 20 Oct 2024 20:47:08 +0200 Subject: [PATCH] Add As* methods to fetch.J --- README.md | 50 ++++++++++++++++++----------- error.go | 19 +++++------ j.go | 74 +++++++++++++++++++++++++++++++++++++++++- j_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++- parse.go | 13 ++++++++ parse_test.go | 16 +++++++++ 6 files changed, 231 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 48ad4dd..a09fdaa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/error.go b/error.go index bf1d966..ad08dd1 100644 --- a/error.go +++ b/error.go @@ -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 } @@ -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 { @@ -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...)} diff --git a/j.go b/j.go index 464934e..d1b0d66 100644 --- a/j.go +++ b/j.go @@ -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. @@ -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. @@ -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 @@ -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) @@ -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 @@ -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 @@ -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 @@ -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, "[") @@ -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 diff --git a/j_test.go b/j_test.go index 8f0b3cc..cf79321 100644 --- a/j_test.go +++ b/j_test.go @@ -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") } @@ -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 +} diff --git a/parse.go b/parse.go index 4f71af1..d78f03e 100644 --- a/parse.go +++ b/parse.go @@ -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 diff --git a/parse_test.go b/parse_test.go index e41e040..a034da7 100644 --- a/parse_test.go +++ b/parse_test.go @@ -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) + } +}