mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-18 12:23:09 -06:00
Use gjson@1.6.0 as it fixes https://github.com/tidwall/gjson/issues/157
This commit is contained in:
parent
ab81b95eae
commit
d2ffa05867
|
|
@ -1,20 +0,0 @@
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2016 Josh Baker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
495
gjson/README.md
495
gjson/README.md
|
|
@ -1,495 +0,0 @@
|
||||||
<p align="center">
|
|
||||||
<img
|
|
||||||
src="logo.png"
|
|
||||||
width="240" height="78" border="0" alt="GJSON">
|
|
||||||
<br>
|
|
||||||
<a href="https://travis-ci.org/tidwall/gjson"><img src="https://img.shields.io/travis/tidwall/gjson.svg?style=flat-square" alt="Build Status"></a>
|
|
||||||
<a href="https://godoc.org/github.com/tidwall/gjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
|
|
||||||
<a href="http://tidwall.com/gjson-play"><img src="https://img.shields.io/badge/%F0%9F%8F%90-playground-9900cc.svg?style=flat-square" alt="GJSON Playground"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">get json values quickly</a></p>
|
|
||||||
|
|
||||||
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), and [parsing json lines](#json-lines).
|
|
||||||
|
|
||||||
Also check out [SJSON](https://github.com/tidwall/sjson) for modifying json, and the [JJ](https://github.com/tidwall/jj) command line tool.
|
|
||||||
|
|
||||||
Getting Started
|
|
||||||
===============
|
|
||||||
|
|
||||||
## Installing
|
|
||||||
|
|
||||||
To start using GJSON, install Go and run `go get`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ go get -u github.com/tidwall/gjson
|
|
||||||
```
|
|
||||||
|
|
||||||
This will retrieve the library.
|
|
||||||
|
|
||||||
## Get a value
|
|
||||||
Get searches json for the specified path. A path is in dot syntax, such as "name.last" or "age". When the value is found it's returned immediately.
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "github.com/tidwall/gjson"
|
|
||||||
|
|
||||||
const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
value := gjson.Get(json, "name.last")
|
|
||||||
println(value.String())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This will print:
|
|
||||||
|
|
||||||
```
|
|
||||||
Prichard
|
|
||||||
```
|
|
||||||
*There's also the [GetMany](#get-multiple-values-at-once) function to get multiple values at once, and [GetBytes](#working-with-bytes) for working with JSON byte slices.*
|
|
||||||
|
|
||||||
## Path Syntax
|
|
||||||
|
|
||||||
Below is a quick overview of the path syntax, for more complete information please
|
|
||||||
check out [GJSON Syntax](SYNTAX.md).
|
|
||||||
|
|
||||||
A path is a series of keys separated by a dot.
|
|
||||||
A key may contain special wildcard characters '\*' and '?'.
|
|
||||||
To access an array value use the index as the key.
|
|
||||||
To get the number of elements in an array or to access a child path, use the '#' character.
|
|
||||||
The dot and wildcard characters can be escaped with '\\'.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": {"first": "Tom", "last": "Anderson"},
|
|
||||||
"age":37,
|
|
||||||
"children": ["Sara","Alex","Jack"],
|
|
||||||
"fav.movie": "Deer Hunter",
|
|
||||||
"friends": [
|
|
||||||
{"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
|
|
||||||
{"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
|
|
||||||
{"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
```
|
|
||||||
"name.last" >> "Anderson"
|
|
||||||
"age" >> 37
|
|
||||||
"children" >> ["Sara","Alex","Jack"]
|
|
||||||
"children.#" >> 3
|
|
||||||
"children.1" >> "Alex"
|
|
||||||
"child*.2" >> "Jack"
|
|
||||||
"c?ildren.0" >> "Sara"
|
|
||||||
"fav\.movie" >> "Deer Hunter"
|
|
||||||
"friends.#.first" >> ["Dale","Roger","Jane"]
|
|
||||||
"friends.1.last" >> "Craig"
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also query an array for the first match by using `#(...)`, or find all
|
|
||||||
matches with `#(...)#`. Queries support the `==`, `!=`, `<`, `<=`, `>`, `>=`
|
|
||||||
comparison operators and the simple pattern matching `%` (like) and `!%`
|
|
||||||
(not like) operators.
|
|
||||||
|
|
||||||
```
|
|
||||||
friends.#(last=="Murphy").first >> "Dale"
|
|
||||||
friends.#(last=="Murphy")#.first >> ["Dale","Jane"]
|
|
||||||
friends.#(age>45)#.last >> ["Craig","Murphy"]
|
|
||||||
friends.#(first%"D*").last >> "Murphy"
|
|
||||||
friends.#(first!%"D*").last >> "Craig"
|
|
||||||
friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"]
|
|
||||||
```
|
|
||||||
|
|
||||||
*Please note that prior to v1.3.0, queries used the `#[...]` brackets. This was
|
|
||||||
changed in v1.3.0 as to avoid confusion with the new
|
|
||||||
[multipath](SYNTAX.md#multipaths) syntax. For backwards compatibility,
|
|
||||||
`#[...]` will continue to work until the next major release.*
|
|
||||||
|
|
||||||
## Result Type
|
|
||||||
|
|
||||||
GJSON supports the json types `string`, `number`, `bool`, and `null`.
|
|
||||||
Arrays and Objects are returned as their raw json types.
|
|
||||||
|
|
||||||
The `Result` type holds one of these:
|
|
||||||
|
|
||||||
```
|
|
||||||
bool, for JSON booleans
|
|
||||||
float64, for JSON numbers
|
|
||||||
string, for JSON string literals
|
|
||||||
nil, for JSON null
|
|
||||||
```
|
|
||||||
|
|
||||||
To directly access the value:
|
|
||||||
|
|
||||||
```go
|
|
||||||
result.Type // can be String, Number, True, False, Null, or JSON
|
|
||||||
result.Str // holds the string
|
|
||||||
result.Num // holds the float64 number
|
|
||||||
result.Raw // holds the raw json
|
|
||||||
result.Index // index of raw value in original json, zero means index unknown
|
|
||||||
```
|
|
||||||
|
|
||||||
There are a variety of handy functions that work on a result:
|
|
||||||
|
|
||||||
```go
|
|
||||||
result.Exists() bool
|
|
||||||
result.Value() interface{}
|
|
||||||
result.Int() int64
|
|
||||||
result.Uint() uint64
|
|
||||||
result.Float() float64
|
|
||||||
result.String() string
|
|
||||||
result.Bool() bool
|
|
||||||
result.Time() time.Time
|
|
||||||
result.Array() []gjson.Result
|
|
||||||
result.Map() map[string]gjson.Result
|
|
||||||
result.Get(path string) Result
|
|
||||||
result.ForEach(iterator func(key, value Result) bool)
|
|
||||||
result.Less(token Result, caseSensitive bool) bool
|
|
||||||
```
|
|
||||||
|
|
||||||
The `result.Value()` function returns an `interface{}` which requires type assertion and is one of the following Go types:
|
|
||||||
|
|
||||||
The `result.Array()` function returns back an array of values.
|
|
||||||
If the result represents a non-existent value, then an empty array will be returned.
|
|
||||||
If the result is not a JSON array, the return value will be an array containing one result.
|
|
||||||
|
|
||||||
```go
|
|
||||||
boolean >> bool
|
|
||||||
number >> float64
|
|
||||||
string >> string
|
|
||||||
null >> nil
|
|
||||||
array >> []interface{}
|
|
||||||
object >> map[string]interface{}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 64-bit integers
|
|
||||||
|
|
||||||
The `result.Int()` and `result.Uint()` calls are capable of reading all 64 bits, allowing for large JSON integers.
|
|
||||||
|
|
||||||
```go
|
|
||||||
result.Int() int64 // -9223372036854775808 to 9223372036854775807
|
|
||||||
result.Uint() int64 // 0 to 18446744073709551615
|
|
||||||
```
|
|
||||||
|
|
||||||
## Modifiers and path chaining
|
|
||||||
|
|
||||||
New in version 1.2 is support for modifier functions and path chaining.
|
|
||||||
|
|
||||||
A modifier is a path component that performs custom processing on the
|
|
||||||
json.
|
|
||||||
|
|
||||||
Multiple paths can be "chained" together using the pipe character.
|
|
||||||
This is useful for getting results from a modified query.
|
|
||||||
|
|
||||||
For example, using the built-in `@reverse` modifier on the above json document,
|
|
||||||
we'll get `children` array and reverse the order:
|
|
||||||
|
|
||||||
```
|
|
||||||
"children|@reverse" >> ["Jack","Alex","Sara"]
|
|
||||||
"children|@reverse|0" >> "Jack"
|
|
||||||
```
|
|
||||||
|
|
||||||
There are currently the following built-in modifiers:
|
|
||||||
|
|
||||||
- `@reverse`: Reverse an array or the members of an object.
|
|
||||||
- `@ugly`: Remove all whitespace from a json document.
|
|
||||||
- `@pretty`: Make the json document more human readable.
|
|
||||||
- `@this`: Returns the current element. It can be used to retrieve the root element.
|
|
||||||
- `@valid`: Ensure the json document is valid.
|
|
||||||
- `@flatten`: Flattens an array.
|
|
||||||
- `@join`: Joins multiple objects into a single object.
|
|
||||||
|
|
||||||
### Modifier arguments
|
|
||||||
|
|
||||||
A modifier may accept an optional argument. The argument can be a valid JSON
|
|
||||||
document or just characters.
|
|
||||||
|
|
||||||
For example, the `@pretty` modifier takes a json object as its argument.
|
|
||||||
|
|
||||||
```
|
|
||||||
@pretty:{"sortKeys":true}
|
|
||||||
```
|
|
||||||
|
|
||||||
Which makes the json pretty and orders all of its keys.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"age":37,
|
|
||||||
"children": ["Sara","Alex","Jack"],
|
|
||||||
"fav.movie": "Deer Hunter",
|
|
||||||
"friends": [
|
|
||||||
{"age": 44, "first": "Dale", "last": "Murphy"},
|
|
||||||
{"age": 68, "first": "Roger", "last": "Craig"},
|
|
||||||
{"age": 47, "first": "Jane", "last": "Murphy"}
|
|
||||||
],
|
|
||||||
"name": {"first": "Tom", "last": "Anderson"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
*The full list of `@pretty` options are `sortKeys`, `indent`, `prefix`, and `width`.
|
|
||||||
Please see [Pretty Options](https://github.com/tidwall/pretty#customized-output) for more information.*
|
|
||||||
|
|
||||||
### Custom modifiers
|
|
||||||
|
|
||||||
You can also add custom modifiers.
|
|
||||||
|
|
||||||
For example, here we create a modifier that makes the entire json document upper
|
|
||||||
or lower case.
|
|
||||||
|
|
||||||
```go
|
|
||||||
gjson.AddModifier("case", func(json, arg string) string {
|
|
||||||
if arg == "upper" {
|
|
||||||
return strings.ToUpper(json)
|
|
||||||
}
|
|
||||||
if arg == "lower" {
|
|
||||||
return strings.ToLower(json)
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
"children|@case:upper" >> ["SARA","ALEX","JACK"]
|
|
||||||
"children|@case:lower|@reverse" >> ["jack","alex","sara"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## JSON Lines
|
|
||||||
|
|
||||||
There's support for [JSON Lines](http://jsonlines.org/) using the `..` prefix, which treats a multilined 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 JSON lines.
|
|
||||||
|
|
||||||
```go
|
|
||||||
gjson.ForEachLine(json, func(line gjson.Result) bool{
|
|
||||||
println(line.String())
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Get nested array values
|
|
||||||
|
|
||||||
Suppose you want all the last names from the following json:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"programmers": [
|
|
||||||
{
|
|
||||||
"firstName": "Janet",
|
|
||||||
"lastName": "McLaughlin",
|
|
||||||
}, {
|
|
||||||
"firstName": "Elliotte",
|
|
||||||
"lastName": "Hunter",
|
|
||||||
}, {
|
|
||||||
"firstName": "Jason",
|
|
||||||
"lastName": "Harold",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You would use the path "programmers.#.lastName" like such:
|
|
||||||
|
|
||||||
```go
|
|
||||||
result := gjson.Get(json, "programmers.#.lastName")
|
|
||||||
for _, name := range result.Array() {
|
|
||||||
println(name.String())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also query an object inside an array:
|
|
||||||
|
|
||||||
```go
|
|
||||||
name := gjson.Get(json, `programmers.#(lastName="Hunter").firstName`)
|
|
||||||
println(name.String()) // prints "Elliotte"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Iterate through an object or array
|
|
||||||
|
|
||||||
The `ForEach` function allows for quickly iterating through an object or array.
|
|
||||||
The key and value are passed to the iterator function for objects.
|
|
||||||
Only the value is passed for arrays.
|
|
||||||
Returning `false` from an iterator will stop iteration.
|
|
||||||
|
|
||||||
```go
|
|
||||||
result := gjson.Get(json, "programmers")
|
|
||||||
result.ForEach(func(key, value gjson.Result) bool {
|
|
||||||
println(value.String())
|
|
||||||
return true // keep iterating
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Simple Parse and Get
|
|
||||||
|
|
||||||
There's a `Parse(json)` function that will do a simple parse, and `result.Get(path)` that will search a result.
|
|
||||||
|
|
||||||
For example, all of these will return the same result:
|
|
||||||
|
|
||||||
```go
|
|
||||||
gjson.Parse(json).Get("name").Get("last")
|
|
||||||
gjson.Get(json, "name").Get("last")
|
|
||||||
gjson.Get(json, "name.last")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Check for the existence of a value
|
|
||||||
|
|
||||||
Sometimes you just want to know if a value exists.
|
|
||||||
|
|
||||||
```go
|
|
||||||
value := gjson.Get(json, "name.last")
|
|
||||||
if !value.Exists() {
|
|
||||||
println("no last name")
|
|
||||||
} else {
|
|
||||||
println(value.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Or as one step
|
|
||||||
if gjson.Get(json, "name.last").Exists() {
|
|
||||||
println("has a last name")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Validate JSON
|
|
||||||
|
|
||||||
The `Get*` and `Parse*` functions expects that the json is well-formed. Bad json will not panic, but it may return back unexpected results.
|
|
||||||
|
|
||||||
If you are consuming JSON from an unpredictable source then you may want to validate prior to using GJSON.
|
|
||||||
|
|
||||||
```go
|
|
||||||
if !gjson.Valid(json) {
|
|
||||||
return errors.New("invalid json")
|
|
||||||
}
|
|
||||||
value := gjson.Get(json, "name.last")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Unmarshal to a map
|
|
||||||
|
|
||||||
To unmarshal to a `map[string]interface{}`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
m, ok := gjson.Parse(json).Value().(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
// not a map
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Working with Bytes
|
|
||||||
|
|
||||||
If your JSON is contained in a `[]byte` slice, there's the [GetBytes](https://godoc.org/github.com/tidwall/gjson#GetBytes) function. This is preferred over `Get(string(data), path)`.
|
|
||||||
|
|
||||||
```go
|
|
||||||
var json []byte = ...
|
|
||||||
result := gjson.GetBytes(json, path)
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are using the `gjson.GetBytes(json, path)` function and you want to avoid converting `result.Raw` to a `[]byte`, then you can use this pattern:
|
|
||||||
|
|
||||||
```go
|
|
||||||
var json []byte = ...
|
|
||||||
result := gjson.GetBytes(json, path)
|
|
||||||
var raw []byte
|
|
||||||
if result.Index > 0 {
|
|
||||||
raw = json[result.Index:result.Index+len(result.Raw)]
|
|
||||||
} else {
|
|
||||||
raw = []byte(result.Raw)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is a best-effort no allocation sub slice of the original json. This method utilizes the `result.Index` field, which is the position of the raw data in the original json. It's possible that the value of `result.Index` equals zero, in which case the `result.Raw` is converted to a `[]byte`.
|
|
||||||
|
|
||||||
## Get multiple values at once
|
|
||||||
|
|
||||||
The `GetMany` function can be used to get multiple values at the same time.
|
|
||||||
|
|
||||||
```go
|
|
||||||
results := gjson.GetMany(json, "name.first", "name.last", "age")
|
|
||||||
```
|
|
||||||
|
|
||||||
The return value is a `[]Result`, which will always contain exactly the same number of items as the input paths.
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/),
|
|
||||||
[ffjson](https://github.com/pquerna/ffjson),
|
|
||||||
[EasyJSON](https://github.com/mailru/easyjson),
|
|
||||||
[jsonparser](https://github.com/buger/jsonparser),
|
|
||||||
and [json-iterator](https://github.com/json-iterator/go)
|
|
||||||
|
|
||||||
```
|
|
||||||
BenchmarkGJSONGet-8 3000000 372 ns/op 0 B/op 0 allocs/op
|
|
||||||
BenchmarkGJSONUnmarshalMap-8 900000 4154 ns/op 1920 B/op 26 allocs/op
|
|
||||||
BenchmarkJSONUnmarshalMap-8 600000 9019 ns/op 3048 B/op 69 allocs/op
|
|
||||||
BenchmarkJSONDecoder-8 300000 14120 ns/op 4224 B/op 184 allocs/op
|
|
||||||
BenchmarkFFJSONLexer-8 1500000 3111 ns/op 896 B/op 8 allocs/op
|
|
||||||
BenchmarkEasyJSONLexer-8 3000000 887 ns/op 613 B/op 6 allocs/op
|
|
||||||
BenchmarkJSONParserGet-8 3000000 499 ns/op 21 B/op 0 allocs/op
|
|
||||||
BenchmarkJSONIterator-8 3000000 812 ns/op 544 B/op 9 allocs/op
|
|
||||||
```
|
|
||||||
|
|
||||||
JSON document used:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"widget": {
|
|
||||||
"debug": "on",
|
|
||||||
"window": {
|
|
||||||
"title": "Sample Konfabulator Widget",
|
|
||||||
"name": "main_window",
|
|
||||||
"width": 500,
|
|
||||||
"height": 500
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"src": "Images/Sun.png",
|
|
||||||
"hOffset": 250,
|
|
||||||
"vOffset": 250,
|
|
||||||
"alignment": "center"
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"data": "Click Here",
|
|
||||||
"size": 36,
|
|
||||||
"style": "bold",
|
|
||||||
"vOffset": 100,
|
|
||||||
"alignment": "center",
|
|
||||||
"onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Each operation was rotated though one of the following search paths:
|
|
||||||
|
|
||||||
```
|
|
||||||
widget.window.name
|
|
||||||
widget.image.hOffset
|
|
||||||
widget.text.onMouseUp
|
|
||||||
```
|
|
||||||
|
|
||||||
*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.8 and can be be found [here](https://github.com/tidwall/gjson-benchmarks).*
|
|
||||||
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
Josh Baker [@tidwall](http://twitter.com/tidwall)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
GJSON source code is available under the MIT [License](/LICENSE).
|
|
||||||
269
gjson/SYNTAX.md
269
gjson/SYNTAX.md
|
|
@ -1,269 +0,0 @@
|
||||||
# GJSON Path Syntax
|
|
||||||
|
|
||||||
A GJSON Path is a text string syntax that describes a search pattern for quickly retreiving values from a JSON payload.
|
|
||||||
|
|
||||||
This document is designed to explain the structure of a GJSON Path through examples.
|
|
||||||
|
|
||||||
- [Path structure](#path-structure)
|
|
||||||
- [Basic](#basic)
|
|
||||||
- [Wildcards](#wildcards)
|
|
||||||
- [Escape Character](#escape-character)
|
|
||||||
- [Arrays](#arrays)
|
|
||||||
- [Queries](#queries)
|
|
||||||
- [Dot vs Pipe](#dot-vs-pipe)
|
|
||||||
- [Modifiers](#modifiers)
|
|
||||||
- [Multipaths](#multipaths)
|
|
||||||
|
|
||||||
The definitive implemenation is [github.com/tidwall/gjson](https://github.com/tidwall/gjson).
|
|
||||||
Use the [GJSON Playground](https://gjson.dev) to experiment with the syntax online.
|
|
||||||
|
|
||||||
|
|
||||||
## Path structure
|
|
||||||
|
|
||||||
A GJSON Path is intended to be easily expressed as a series of components seperated by a `.` character.
|
|
||||||
|
|
||||||
Along with `.` character, there are a few more that have special meaning, including `|`, `#`, `@`, `\`, `*`, and `?`.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
Given this JSON
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": {"first": "Tom", "last": "Anderson"},
|
|
||||||
"age":37,
|
|
||||||
"children": ["Sara","Alex","Jack"],
|
|
||||||
"fav.movie": "Deer Hunter",
|
|
||||||
"friends": [
|
|
||||||
{"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
|
|
||||||
{"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
|
|
||||||
{"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The following GJSON Paths evaluate to the accompanying values.
|
|
||||||
|
|
||||||
### Basic
|
|
||||||
|
|
||||||
In many cases you'll just want to retreive values by object name or array index.
|
|
||||||
|
|
||||||
```go
|
|
||||||
name.last "Anderson"
|
|
||||||
name.first "Tom"
|
|
||||||
age 37
|
|
||||||
children ["Sara","Alex","Jack"]
|
|
||||||
children.0 "Sara"
|
|
||||||
children.1 "Alex"
|
|
||||||
friends.1 {"first": "Roger", "last": "Craig", "age": 68}
|
|
||||||
friends.1.first "Roger"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Wildcards
|
|
||||||
|
|
||||||
A key may contain the special wildcard characters `*` and `?`.
|
|
||||||
The `*` will match on any zero+ characters, and `?` matches on any one character.
|
|
||||||
|
|
||||||
```go
|
|
||||||
child*.2 "Jack"
|
|
||||||
c?ildren.0 "Sara"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Escape character
|
|
||||||
|
|
||||||
Special purpose characters, such as `.`, `*`, and `?` can be escaped with `\`.
|
|
||||||
|
|
||||||
```go
|
|
||||||
fav\.movie "Deer Hunter"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Arrays
|
|
||||||
|
|
||||||
The `#` character allows for digging into JSON Arrays.
|
|
||||||
|
|
||||||
To get the length of an array you'll just use the `#` all by itself.
|
|
||||||
|
|
||||||
```go
|
|
||||||
friends.# 3
|
|
||||||
friends.#.age [44,68,47]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Queries
|
|
||||||
|
|
||||||
You can also query an array for the first match by using `#(...)`, or find all matches with `#(...)#`.
|
|
||||||
Queries support the `==`, `!=`, `<`, `<=`, `>`, `>=` comparison operators,
|
|
||||||
and the simple pattern matching `%` (like) and `!%` (not like) operators.
|
|
||||||
|
|
||||||
```go
|
|
||||||
friends.#(last=="Murphy").first "Dale"
|
|
||||||
friends.#(last=="Murphy")#.first ["Dale","Jane"]
|
|
||||||
friends.#(age>45)#.last ["Craig","Murphy"]
|
|
||||||
friends.#(first%"D*").last "Murphy"
|
|
||||||
friends.#(first!%"D*").last "Craig"
|
|
||||||
```
|
|
||||||
|
|
||||||
To query for a non-object value in an array, you can forgo the string to the right of the operator.
|
|
||||||
|
|
||||||
```go
|
|
||||||
children.#(!%"*a*") "Alex"
|
|
||||||
children.#(%"*a*")# ["Sara","Jack"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Nested queries are allowed.
|
|
||||||
|
|
||||||
```go
|
|
||||||
friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"]
|
|
||||||
```
|
|
||||||
|
|
||||||
*Please note that prior to v1.3.0, queries used the `#[...]` brackets. This was
|
|
||||||
changed in v1.3.0 as to avoid confusion with the new [multipath](#multipaths)
|
|
||||||
syntax. For backwards compatibility, `#[...]` will continue to work until the
|
|
||||||
next major release.*
|
|
||||||
|
|
||||||
### Dot vs Pipe
|
|
||||||
|
|
||||||
The `.` is standard separator, but it's also possible to use a `|`.
|
|
||||||
In most cases they both end up returning the same results.
|
|
||||||
The cases where`|` differs from `.` is when it's used after the `#` for [Arrays](#arrays) and [Queries](#queries).
|
|
||||||
|
|
||||||
Here are some examples
|
|
||||||
|
|
||||||
```go
|
|
||||||
friends.0.first "Dale"
|
|
||||||
friends|0.first "Dale"
|
|
||||||
friends.0|first "Dale"
|
|
||||||
friends|0|first "Dale"
|
|
||||||
friends|# 3
|
|
||||||
friends.# 3
|
|
||||||
friends.#(last="Murphy")# [{"first": "Dale", "last": "Murphy", "age": 44},{"first": "Jane", "last": "Murphy", "age": 47}]
|
|
||||||
friends.#(last="Murphy")#.first ["Dale","Jane"]
|
|
||||||
friends.#(last="Murphy")#|first <non-existent>
|
|
||||||
friends.#(last="Murphy")#.0 []
|
|
||||||
friends.#(last="Murphy")#|0 {"first": "Dale", "last": "Murphy", "age": 44}
|
|
||||||
friends.#(last="Murphy")#.# []
|
|
||||||
friends.#(last="Murphy")#|# 2
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's break down a few of these.
|
|
||||||
|
|
||||||
The path `friends.#(last="Murphy")#` all by itself results in
|
|
||||||
|
|
||||||
```json
|
|
||||||
[{"first": "Dale", "last": "Murphy", "age": 44},{"first": "Jane", "last": "Murphy", "age": 47}]
|
|
||||||
```
|
|
||||||
|
|
||||||
The `.first` suffix will process the `first` path on each array element *before* returning the results. Which becomes
|
|
||||||
|
|
||||||
```json
|
|
||||||
["Dale","Jane"]
|
|
||||||
```
|
|
||||||
|
|
||||||
But the `|first` suffix actually processes the `first` path *after* the previous result.
|
|
||||||
Since the previous result is an array, not an object, it's not possible to process
|
|
||||||
because `first` does not exist.
|
|
||||||
|
|
||||||
Yet, `|0` suffix returns
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"first": "Dale", "last": "Murphy", "age": 44}
|
|
||||||
```
|
|
||||||
|
|
||||||
Because `0` is the first index of the previous result.
|
|
||||||
|
|
||||||
### Modifiers
|
|
||||||
|
|
||||||
A modifier is a path component that performs custom processing on the JSON.
|
|
||||||
|
|
||||||
For example, using the built-in `@reverse` modifier on the above JSON payload will reverse the `children` array:
|
|
||||||
|
|
||||||
```go
|
|
||||||
children.@reverse ["Jack","Alex","Sara"]
|
|
||||||
children.@reverse.0 "Jack"
|
|
||||||
```
|
|
||||||
|
|
||||||
There are currently the following built-in modifiers:
|
|
||||||
|
|
||||||
- `@reverse`: Reverse an array or the members of an object.
|
|
||||||
- `@ugly`: Remove all whitespace from JSON.
|
|
||||||
- `@pretty`: Make the JSON more human readable.
|
|
||||||
- `@this`: Returns the current element. It can be used to retrieve the root element.
|
|
||||||
- `@valid`: Ensure the json document is valid.
|
|
||||||
- `@flatten`: Flattens an array.
|
|
||||||
- `@join`: Joins multiple objects into a single object.
|
|
||||||
|
|
||||||
#### Modifier arguments
|
|
||||||
|
|
||||||
A modifier may accept an optional argument. The argument can be a valid JSON payload or just characters.
|
|
||||||
|
|
||||||
For example, the `@pretty` modifier takes a json object as its argument.
|
|
||||||
|
|
||||||
```
|
|
||||||
@pretty:{"sortKeys":true}
|
|
||||||
```
|
|
||||||
|
|
||||||
Which makes the json pretty and orders all of its keys.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"age":37,
|
|
||||||
"children": ["Sara","Alex","Jack"],
|
|
||||||
"fav.movie": "Deer Hunter",
|
|
||||||
"friends": [
|
|
||||||
{"age": 44, "first": "Dale", "last": "Murphy"},
|
|
||||||
{"age": 68, "first": "Roger", "last": "Craig"},
|
|
||||||
{"age": 47, "first": "Jane", "last": "Murphy"}
|
|
||||||
],
|
|
||||||
"name": {"first": "Tom", "last": "Anderson"}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
*The full list of `@pretty` options are `sortKeys`, `indent`, `prefix`, and `width`.
|
|
||||||
Please see [Pretty Options](https://github.com/tidwall/pretty#customized-output) for more information.*
|
|
||||||
|
|
||||||
#### Custom modifiers
|
|
||||||
|
|
||||||
You can also add custom modifiers.
|
|
||||||
|
|
||||||
For example, here we create a modifier which makes the entire JSON payload upper or lower case.
|
|
||||||
|
|
||||||
```go
|
|
||||||
gjson.AddModifier("case", func(json, arg string) string {
|
|
||||||
if arg == "upper" {
|
|
||||||
return strings.ToUpper(json)
|
|
||||||
}
|
|
||||||
if arg == "lower" {
|
|
||||||
return strings.ToLower(json)
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
})
|
|
||||||
"children.@case:upper" ["SARA","ALEX","JACK"]
|
|
||||||
"children.@case:lower.@reverse" ["jack","alex","sara"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multipaths
|
|
||||||
|
|
||||||
Starting with v1.3.0, GJSON added the ability to join multiple paths together
|
|
||||||
to form new documents. Wrapping comma-separated paths between `{...}` or
|
|
||||||
`[...]` will result in a new array or object, respectively.
|
|
||||||
|
|
||||||
For example, using the given multipath
|
|
||||||
|
|
||||||
```
|
|
||||||
{name.first,age,"the_murphys":friends.#(last="Murphy")#.first}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we selected the first name, age, and the first name for friends with the
|
|
||||||
last name "Murphy".
|
|
||||||
|
|
||||||
You'll notice that an optional key can be provided, in this case
|
|
||||||
"the_murphys", to force assign a key to a value. Otherwise, the name of the
|
|
||||||
actual field will be used, in this case "first". If a name cannot be
|
|
||||||
determined, then "_" is used.
|
|
||||||
|
|
||||||
This results in
|
|
||||||
|
|
||||||
```
|
|
||||||
{"first":"Tom","age":37,"the_murphys":["Dale","Jane"]}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
2824
gjson/gjson.go
2824
gjson/gjson.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,34 +0,0 @@
|
||||||
//+build appengine js
|
|
||||||
|
|
||||||
package gjson
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getBytes(json []byte, path string) Result {
|
|
||||||
return Get(string(json), path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fillIndex finds the position of Raw data and assigns it to the Index field
|
|
||||||
// of the resulting value. If the position cannot be found then Index zero is
|
|
||||||
// used instead.
|
|
||||||
func fillIndex(json string, c *parseContext) {
|
|
||||||
if len(c.value.Raw) > 0 && !c.calcd {
|
|
||||||
jhdr := *(*reflect.StringHeader)(unsafe.Pointer(&json))
|
|
||||||
rhdr := *(*reflect.StringHeader)(unsafe.Pointer(&(c.value.Raw)))
|
|
||||||
c.value.Index = int(rhdr.Data - jhdr.Data)
|
|
||||||
if c.value.Index < 0 || c.value.Index >= len(json) {
|
|
||||||
c.value.Index = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringBytes(s string) []byte {
|
|
||||||
return []byte(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytesString(b []byte) string {
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
//+build !appengine
|
|
||||||
//+build !js
|
|
||||||
|
|
||||||
package gjson
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// getBytes casts the input json bytes to a string and safely returns the
|
|
||||||
// results as uniquely allocated data. This operation is intended to minimize
|
|
||||||
// copies and allocations for the large json string->[]byte.
|
|
||||||
func getBytes(json []byte, path string) Result {
|
|
||||||
var result Result
|
|
||||||
if json != nil {
|
|
||||||
// unsafe cast to string
|
|
||||||
result = Get(*(*string)(unsafe.Pointer(&json)), path)
|
|
||||||
// safely get the string headers
|
|
||||||
rawhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Raw))
|
|
||||||
strhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Str))
|
|
||||||
// create byte slice headers
|
|
||||||
rawh := reflect.SliceHeader{Data: rawhi.Data, Len: rawhi.Len}
|
|
||||||
strh := reflect.SliceHeader{Data: strhi.Data, Len: strhi.Len}
|
|
||||||
if strh.Data == 0 {
|
|
||||||
// str is nil
|
|
||||||
if rawh.Data == 0 {
|
|
||||||
// raw is nil
|
|
||||||
result.Raw = ""
|
|
||||||
} else {
|
|
||||||
// raw has data, safely copy the slice header to a string
|
|
||||||
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
|
|
||||||
}
|
|
||||||
result.Str = ""
|
|
||||||
} else if rawh.Data == 0 {
|
|
||||||
// raw is nil
|
|
||||||
result.Raw = ""
|
|
||||||
// str has data, safely copy the slice header to a string
|
|
||||||
result.Str = string(*(*[]byte)(unsafe.Pointer(&strh)))
|
|
||||||
} else if strh.Data >= rawh.Data &&
|
|
||||||
int(strh.Data)+strh.Len <= int(rawh.Data)+rawh.Len {
|
|
||||||
// Str is a substring of Raw.
|
|
||||||
start := int(strh.Data - rawh.Data)
|
|
||||||
// safely copy the raw slice header
|
|
||||||
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
|
|
||||||
// substring the raw
|
|
||||||
result.Str = result.Raw[start : start+strh.Len]
|
|
||||||
} else {
|
|
||||||
// safely copy both the raw and str slice headers to strings
|
|
||||||
result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh)))
|
|
||||||
result.Str = string(*(*[]byte)(unsafe.Pointer(&strh)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// fillIndex finds the position of Raw data and assigns it to the Index field
|
|
||||||
// of the resulting value. If the position cannot be found then Index zero is
|
|
||||||
// used instead.
|
|
||||||
func fillIndex(json string, c *parseContext) {
|
|
||||||
if len(c.value.Raw) > 0 && !c.calcd {
|
|
||||||
jhdr := *(*reflect.StringHeader)(unsafe.Pointer(&json))
|
|
||||||
rhdr := *(*reflect.StringHeader)(unsafe.Pointer(&(c.value.Raw)))
|
|
||||||
c.value.Index = int(rhdr.Data - jhdr.Data)
|
|
||||||
if c.value.Index < 0 || c.value.Index >= len(json) {
|
|
||||||
c.value.Index = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringBytes(s string) []byte {
|
|
||||||
return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
|
|
||||||
Data: (*reflect.StringHeader)(unsafe.Pointer(&s)).Data,
|
|
||||||
Len: len(s),
|
|
||||||
Cap: len(s),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytesString(b []byte) string {
|
|
||||||
return *(*string)(unsafe.Pointer(&b))
|
|
||||||
}
|
|
||||||
2164
gjson/gjson_test.go
2164
gjson/gjson_test.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +0,0 @@
|
||||||
module github.com/tidwall/gjson
|
|
||||||
|
|
||||||
go 1.12
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/tidwall/match v1.0.1
|
|
||||||
github.com/tidwall/pretty v1.0.0
|
|
||||||
)
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
|
||||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
|
||||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
|
||||||
BIN
gjson/logo.png
BIN
gjson/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
3
go.mod
3
go.mod
|
|
@ -4,8 +4,6 @@ replace github.com/lib/pq => github.com/matrix-org/pq v1.3.2
|
||||||
|
|
||||||
replace github.com/prometheus/client_golang => ./prometheus
|
replace github.com/prometheus/client_golang => ./prometheus
|
||||||
|
|
||||||
replace github.com/tidwall/gjson => ./gjson
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/mux v1.7.3
|
github.com/gorilla/mux v1.7.3
|
||||||
github.com/lib/pq v1.2.0
|
github.com/lib/pq v1.2.0
|
||||||
|
|
@ -22,6 +20,7 @@ require (
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/prometheus/client_golang v1.4.1
|
github.com/prometheus/client_golang v1.4.1
|
||||||
github.com/sirupsen/logrus v1.4.2
|
github.com/sirupsen/logrus v1.4.2
|
||||||
|
github.com/tidwall/gjson v1.6.0
|
||||||
github.com/tidwall/pretty v1.0.1 // indirect
|
github.com/tidwall/pretty v1.0.1 // indirect
|
||||||
github.com/uber/jaeger-client-go v2.22.1+incompatible
|
github.com/uber/jaeger-client-go v2.22.1+incompatible
|
||||||
github.com/uber/jaeger-lib v2.2.0+incompatible
|
github.com/uber/jaeger-lib v2.2.0+incompatible
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -88,6 +88,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/tidwall/gjson v1.1.5 h1:QysILxBeUEY3GTLA0fQVgkQG1zme8NxGvhh2SSqWNwI=
|
github.com/tidwall/gjson v1.1.5 h1:QysILxBeUEY3GTLA0fQVgkQG1zme8NxGvhh2SSqWNwI=
|
||||||
github.com/tidwall/gjson v1.1.5/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=
|
github.com/tidwall/gjson v1.1.5/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=
|
||||||
|
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
|
||||||
|
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
|
||||||
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
||||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue