Skip to content

Commit

Permalink
Merge pull request #5 from glossd/add-nil-j-type
Browse files Browse the repository at this point in the history
Add Nil type
  • Loading branch information
glossd authored Oct 19, 2024
2 parents 2fbf696 + 40c2c6b commit f7971a2
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 47 deletions.
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,24 @@ 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 input the unmarshalled `fetch.J` could be one of these types implementing `fetch.J`

| Type | Definition |
|---------|-----------------|
| fetch.M | map[string]any |
| fetch.A | []any |
| fetch.F | float64 |
| fetch.S | string |
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.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()`.
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`
```go
j, _ := fetch.Unmarshal[fetch.J]("{}")
if j.Q(".name").Raw() == nil {
// key 'name' doesn't exist
}
```

Method `fetch.J#Q` returns `fetch.J`. You can use the method `Q` on the result as well.
```go
Expand Down
51 changes: 38 additions & 13 deletions j.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"strings"
)

var jnil Nil

type J interface {
// Q parses JQ-like patterns and returns according to the path value.
// E.g.
Expand All @@ -21,25 +23,26 @@ type J interface {
// 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(pattern string) J

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

// Raw converts the value to its definition and returns it.
// Check the value on nil because Q might return nil value. In Golang
// calling a method on a nil interface is a run-time error.
// Type definitions:
// Type-Definitions:
// M -> map[string]any
// A -> []any
// F -> float64
// S -> string
// Nil -> nil
Raw() any
}

// M represents a JSON object.
type M map[string]any

// Q parses JQ-like patterns and returns according to the path value.
// Invalid pattern doesn't error
// Retrieve a field Q(".name")
func (m M) Q(pattern string) J {
if strings.HasPrefix(pattern, ".") {
pattern = pattern[1:]
Expand All @@ -56,7 +59,7 @@ func (m M) Q(pattern string) J {

v, ok := m[key]
if !ok {
return nil
return jnil
}

return parseValue(v, remaining, sep)
Expand All @@ -70,6 +73,7 @@ func (m M) Raw() any {
return map[string]any(m)
}

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

func (a A) Q(pattern string) J {
Expand All @@ -82,7 +86,7 @@ func (a A) Q(pattern string) J {

if pattern[0] != '[' {
// expected array index, got object key
return nil
return jnil
}
closeBracket := strings.Index(pattern, "]")
if closeBracket == -1 {
Expand All @@ -95,7 +99,7 @@ func (a A) Q(pattern string) J {
}
if index < 0 || index >= len(a) {
// index out of range
return nil
return jnil
}

v := a[index]
Expand Down Expand Up @@ -131,13 +135,14 @@ func parseValue(v any, remaining string, sep string) J {
arr, ok := v.([]any)
if !ok {
// expected an array
return nil
return jnil
}
return A(arr).Q(remaining)
}
panic("glossd/fetch panic, please report to github: array only expected . or [ ")
}

// F represents a JSON number
type F float64

func (f F) Q(pattern string) J {
Expand All @@ -147,7 +152,7 @@ func (f F) Q(pattern string) J {
if pattern == "." {
return f
}
return nil
return jnil
}

func (f F) String() string {
Expand All @@ -158,6 +163,7 @@ func (f F) Raw() any {
return float64(f)
}

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

func (s S) Q(pattern string) J {
Expand All @@ -167,7 +173,7 @@ func (s S) Q(pattern string) J {
if pattern == "." {
return s
}
return nil
return jnil
}

func (s S) String() string {
Expand All @@ -178,6 +184,25 @@ func (s S) Raw() any {
return string(s)
}

type nilStruct struct{}

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

func (n Nil) Q(string) J {
return n
}

func (n Nil) String() string {
return "nil"
}

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

func nextSep(pattern string) (int, string) {
dot := strings.Index(pattern, ".")
bracket := strings.Index(pattern, "[")
Expand Down Expand Up @@ -208,7 +233,7 @@ func beforeSep(pattern string) string {

func convert(v any) J {
if v == nil {
return nil
return jnil
}
switch t := v.(type) {
case float64:
Expand Down
27 changes: 22 additions & 5 deletions j_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,7 @@ func TestJ_Q(t *testing.T) {
// fmt.Print()
//}

gotJ := j.Q(c.P)
var got any
if gotJ != nil {
got = gotJ.Raw()
}
got := j.Q(c.P).Raw()
if got != c.E {
t.Errorf("case #%d: wrong value, expected=%v, got=%v", i, c.E, got)
}
Expand Down Expand Up @@ -121,3 +117,24 @@ func TestJ_String(t *testing.T) {
t.Errorf("J.String j inteface wrong value")
}
}

func TestJ_Nil(t *testing.T) {
j, err := Unmarshal[J](`{"name":"Lola"}`)
if err != nil {
t.Fatal(err)
}

if j.Q(".id") == nil {
t.Errorf("didn't expect J value to be nil")
}
fmt.Println(j.Q(".id"))
if j.Q(".id").Raw() != 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 {
t.Errorf("expected id to be nil")
}
}
20 changes: 0 additions & 20 deletions stringify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,3 @@ func TestMarshalStruct(t *testing.T) {
}
}
}

type SA struct {
t *testing.T
expected string
}

func (sa SA) assertMarshal(res string, err error) {
if err != nil {
sa.t.Fatalf("Marshal error: %s", err)
}
if sa.expected != string(res) {
sa.t.Errorf("Marshal result mismatch, got=%s, expected=%s", res, sa.expected)
}
}

func assertNil(a any) {
if a != nil {
panic(a)
}
}

0 comments on commit f7971a2

Please sign in to comment.