Added support for JSON Lines

Added support for JSON Lines (http://jsonlines.org) using the `..` prefix.
Which when specified, treats the multi-lined document as an array.

For example:

```
{"name": "Gilbert", "age": 61}
{"name": "Alexa", "age": 34}
{"name": "May", "age": 57}
{"name": "Deloise", "age": 44}
```

```
..#                   >> 4
..1                   >> {"name": "Alexa", "age": 34}
..3                   >> {"name": "Deloise", "age": 44}
..#.name              >> ["Gilbert","Alexa","May","Deloise"]
..#[name="May"].age   >> 57
```

Closes #60
This commit is contained in:
Josh Baker 2018-02-09 15:41:16 -07:00
parent 5fe9078c47
commit a2f35b522e
3 changed files with 149 additions and 19 deletions

View File

@ -14,6 +14,7 @@
GJSON is a Go package that provides a [fast](#performance) and [simple](#get-a-value) way to get values from a json document. GJSON is a Go package that provides a [fast](#performance) and [simple](#get-a-value) way to get values from a json document.
It has features such as [one line retrieval](#get-a-value), [dot notation paths](#path-syntax), [iteration](#iterate-through-an-object-or-array). It has features such as [one line retrieval](#get-a-value), [dot notation paths](#path-syntax), [iteration](#iterate-through-an-object-or-array).
For a command-line tool that uses the GJSON syntax check out [JJ](https://github.com/tidwall/jj).
Getting Started Getting Started
=============== ===============
@ -95,6 +96,43 @@ friends.#[age>45]#.last >> ["Craig","Murphy"]
friends.#[first%"D*"].last >> "Murphy" friends.#[first%"D*"].last >> "Murphy"
``` ```
## JSON Lines
There also support for [JSON Lines](http://jsonlines.org/) using the `..` prefix.
Which when specified, treats the multi-lined document as an array.
For example:
```
{"name": "Gilbert", "age": 61}
{"name": "Alexa", "age": 34}
{"name": "May", "age": 57}
{"name": "Deloise", "age": 44}
```
```
..# >> 4
..1 >> {"name": "Alexa", "age": 34}
..3 >> {"name": "Deloise", "age": 44}
..#.name >> ["Gilbert","Alexa","May","Deloise"]
..#[name="May"].age >> 57
```
The `ForEachLines` function will iterate through lines.
```go
gjson.ForEachLine(json, func(line gjson.Result) bool{
println(line.String())
return true
})
// Outputs:
// {"name": "Gilbert", "age": 61}
// {"name": "Alexa", "age": 34}
// {"name": "May", "age": 57}
// {"name": "Deloise", "age": 44}
```
## Result Type ## Result Type
GJSON supports the json types `string`, `number`, `bool`, and `null`. GJSON supports the json types `string`, `number`, `bool`, and `null`.

View File

@ -1128,7 +1128,7 @@ func parseArray(c *parseContext, i int, path string) (int, bool) {
partidx = int(n) partidx = int(n)
} }
} }
for i < len(c.json) { for i < len(c.json)+1 {
if !rp.arrch { if !rp.arrch {
pmatch = partidx == h pmatch = partidx == h
hit = pmatch && !rp.more hit = pmatch && !rp.more
@ -1137,8 +1137,16 @@ func parseArray(c *parseContext, i int, path string) (int, bool) {
if rp.alogok { if rp.alogok {
alog = append(alog, i) alog = append(alog, i)
} }
for ; i < len(c.json); i++ { for ; ; i++ {
switch c.json[i] { var ch byte
if i > len(c.json) {
break
} else if i == len(c.json) {
ch = ']'
} else {
ch = c.json[i]
}
switch ch {
default: default:
continue continue
case '"': case '"':
@ -1252,8 +1260,11 @@ func parseArray(c *parseContext, i int, path string) (int, bool) {
if rp.alogok { if rp.alogok {
var jsons = make([]byte, 0, 64) var jsons = make([]byte, 0, 64)
jsons = append(jsons, '[') jsons = append(jsons, '[')
for j, k := 0, 0; j < len(alog); j++ { for j, k := 0, 0; j < len(alog); j++ {
res := Get(c.json[alog[j]:], rp.alogkey) _, res, ok := parseAny(c.json, alog[j], true)
if ok {
res := res.Get(rp.alogkey)
if res.Exists() { if res.Exists() {
if k > 0 { if k > 0 {
jsons = append(jsons, ',') jsons = append(jsons, ',')
@ -1262,6 +1273,7 @@ func parseArray(c *parseContext, i int, path string) (int, bool) {
k++ k++
} }
} }
}
jsons = append(jsons, ']') jsons = append(jsons, ']')
c.value.Type = JSON c.value.Type = JSON
c.value.Raw = string(jsons) c.value.Raw = string(jsons)
@ -1290,10 +1302,28 @@ func parseArray(c *parseContext, i int, path string) (int, bool) {
return i, false return i, false
} }
// ForEachLine iterates through lines of JSON as specified by the JSON Lines
// format (http://jsonlines.org/).
// Each line is returned as a GJSON Result.
func ForEachLine(json string, iterator func(line Result) bool) {
var res Result
var i int
for {
i, res, _ = parseAny(json, i, true)
if !res.Exists() {
break
}
if !iterator(res) {
return
}
}
}
type parseContext struct { type parseContext struct {
json string json string
value Result value Result
calcd bool calcd bool
lines bool
} }
// Get searches json for the specified path. // Get searches json for the specified path.
@ -1329,6 +1359,10 @@ type parseContext struct {
func Get(json, path string) Result { func Get(json, path string) Result {
var i int var i int
var c = &parseContext{json: json} var c = &parseContext{json: json}
if len(path) >= 2 && path[0] == '.' && path[1] == '.' {
c.lines = true
parseArray(c, 0, path[2:])
} else {
for ; i < len(c.json); i++ { for ; i < len(c.json); i++ {
if c.json[i] == '{' { if c.json[i] == '{' {
i++ i++
@ -1341,6 +1375,7 @@ func Get(json, path string) Result {
break break
} }
} }
}
if len(c.value.Raw) > 0 && !c.calcd { if len(c.value.Raw) > 0 && !c.calcd {
jhdr := *(*reflect.StringHeader)(unsafe.Pointer(&json)) jhdr := *(*reflect.StringHeader)(unsafe.Pointer(&json))
rhdr := *(*reflect.StringHeader)(unsafe.Pointer(&(c.value.Raw))) rhdr := *(*reflect.StringHeader)(unsafe.Pointer(&(c.value.Raw)))

View File

@ -1295,3 +1295,60 @@ func TestIssue58(t *testing.T) {
t.Fatalf("expected '%v', got '%v'", `{"uid": 1}`, res) t.Fatalf("expected '%v', got '%v'", `{"uid": 1}`, res)
} }
} }
func TestObjectGrouping(t *testing.T) {
json := `
[
true,
{"name":"tom"},
false,
{"name":"janet"},
null
]
`
res := Get(json, "#.name")
if res.String() != `["tom","janet"]` {
t.Fatalf("expected '%v', got '%v'", `["tom","janet"]`, res.String())
}
}
func TestJSONLines(t *testing.T) {
json := `
true
false
{"name":"tom"}
[1,2,3,4,5]
{"name":"janet"}
null
12930.1203
`
paths := []string{"..#", "..0", "..2.name", "..#.name", "..6", "..7"}
ress := []string{"7", "true", "tom", `["tom","janet"]`, "12930.1203", ""}
for i, path := range paths {
res := Get(json, path)
if res.String() != ress[i] {
t.Fatalf("expected '%v', got '%v'", ress[i], res.String())
}
}
json = `
{"name": "Gilbert", "wins": [["straight", "7♣"], ["one pair", "10♥"]]}
{"name": "Alexa", "wins": [["two pair", "4♠"], ["two pair", "9♠"]]}
{"name": "May", "wins": []}
{"name": "Deloise", "wins": [["three of a kind", "5♣"]]}
`
var i int
lines := strings.Split(strings.TrimSpace(json), "\n")
ForEachLine(json, func(line Result) bool {
if line.Raw != lines[i] {
t.Fatalf("expected '%v', got '%v'", lines[i], line.Raw)
}
i++
return true
})
if i != 4 {
t.Fatalf("expected '%v', got '%v'", 4, i)
}
}