Skip to content

Commit

Permalink
Added new tilde types and fixed ~false
Browse files Browse the repository at this point in the history
This commit fixes an issue with ~false where the it's value was
simply the opposite of ~true. Now ~false explicitly checks for
false-ish values.

Also added ~null and ~* for testing null-ish and non-existent
values.

see #327
  • Loading branch information
tidwall committed Jul 27, 2023
1 parent e14b8d3 commit 8d2c36f
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 68 deletions.
23 changes: 20 additions & 3 deletions SYNTAX.md
Expand Up @@ -137,12 +137,21 @@ next major release.*

The `~` (tilde) operator will convert a value to a boolean before comparison.

Supported tilde comparison type are:

```
~true Converts true-ish values to true
~false Converts false-ish and non-existent values to true
~null Converts null and non-existent values to true
~* Converts any existing value to true
```

For example, using the following JSON:

```json
{
"vals": [
{ "a": 1, "b": true },
{ "a": 1, "b": "data" },
{ "a": 2, "b": true },
{ "a": 3, "b": false },
{ "a": 4, "b": "0" },
Expand All @@ -157,15 +166,23 @@ For example, using the following JSON:
}
```

You can now query for all true(ish) or false(ish) values:
To query for all true-ish or false-ish values:

```
vals.#(b==~true)#.a >> [1,2,6,7,8]
vals.#(b==~true)#.a >> [2,6,7,8]
vals.#(b==~false)#.a >> [3,4,5,9,10,11]
```

The last value which was non-existent is treated as `false`

To query for null and explicit value existence:

```
vals.#(b==~null)#.a >> [10,11]
vals.#(b==~*)#.a >> [1,2,3,4,5,6,7,8,9,10]
vals.#(b!=~*)#.a >> [11]
```

### Dot vs Pipe

The `.` is standard separator, but it's also possible to use a `|`.
Expand Down
202 changes: 140 additions & 62 deletions gjson.go
Expand Up @@ -645,9 +645,9 @@ func tostr(json string) (raw string, str string) {

// Exists returns true if value exists.
//
// if gjson.Get(json, "name.last").Exists(){
// println("value exists")
// }
// if gjson.Get(json, "name.last").Exists(){
// println("value exists")
// }
func (t Result) Exists() bool {
return t.Type != Null || len(t.Raw) != 0
}
Expand All @@ -661,7 +661,6 @@ func (t Result) Exists() bool {
// nil, for JSON null
// map[string]interface{}, for JSON objects
// []interface{}, for JSON arrays
//
func (t Result) Value() interface{} {
if t.Type == String {
return t.Str
Expand Down Expand Up @@ -826,19 +825,28 @@ func parseArrayPath(path string) (r arrayPathResult) {
}

// splitQuery takes a query and splits it into three parts:
// path, op, middle, and right.
//
// path, op, middle, and right.
//
// So for this query:
// #(first_name=="Murphy").last
//
// #(first_name=="Murphy").last
//
// Becomes
// first_name # path
// =="Murphy" # middle
// .last # right
//
// first_name # path
// =="Murphy" # middle
// .last # right
//
// Or,
// #(service_roles.#(=="one")).cap
//
// #(service_roles.#(=="one")).cap
//
// Becomes
// service_roles.#(=="one") # path
// # middle
// .cap # right
//
// service_roles.#(=="one") # path
// # middle
// .cap # right
func parseQuery(query string) (
path, op, value, remain string, i int, vesc, ok bool,
) {
Expand Down Expand Up @@ -1251,15 +1259,74 @@ func matchLimit(str, pattern string) bool {
return matched
}

func falseish(t Result) bool {
switch t.Type {
case Null:
return true
case False:
return true
case String:
b, err := strconv.ParseBool(strings.ToLower(t.Str))
if err != nil {
return false
}
return !b
case Number:
return t.Num == 0
default:
return false
}
}

func trueish(t Result) bool {
switch t.Type {
case True:
return true
case String:
b, err := strconv.ParseBool(strings.ToLower(t.Str))
if err != nil {
return false
}
return b
case Number:
return t.Num != 0
default:
return false
}
}

func nullish(t Result) bool {
return t.Type == Null
}

func queryMatches(rp *arrayPathResult, value Result) bool {
rpv := rp.query.value
if len(rpv) > 0 && rpv[0] == '~' {
// convert to bool
rpv = rpv[1:]
if value.Bool() {
value = Result{Type: True}
} else {
value = Result{Type: False}
if len(rpv) > 0 {
if rpv[0] == '~' {
// convert to bool
rpv = rpv[1:]
var ish, ok bool
switch rpv {
case "*":
ish, ok = value.Exists(), true
case "null":
ish, ok = nullish(value), true
case "true":
ish, ok = trueish(value), true
case "false":
ish, ok = falseish(value), true
}
if ok {
rpv = "true"
if ish {
value = Result{Type: True}
} else {
value = Result{Type: False}
}
} else {
rpv = ""
value = Result{}
}
}
}
if !value.Exists() {
Expand Down Expand Up @@ -1918,23 +1985,23 @@ type parseContext struct {
// the '#' character.
// The dot and wildcard character can be escaped with '\'.
//
// {
// "name": {"first": "Tom", "last": "Anderson"},
// "age":37,
// "children": ["Sara","Alex","Jack"],
// "friends": [
// {"first": "James", "last": "Murphy"},
// {"first": "Roger", "last": "Craig"}
// ]
// }
// "name.last" >> "Anderson"
// "age" >> 37
// "children" >> ["Sara","Alex","Jack"]
// "children.#" >> 3
// "children.1" >> "Alex"
// "child*.2" >> "Jack"
// "c?ildren.0" >> "Sara"
// "friends.#.first" >> ["James","Roger"]
// {
// "name": {"first": "Tom", "last": "Anderson"},
// "age":37,
// "children": ["Sara","Alex","Jack"],
// "friends": [
// {"first": "James", "last": "Murphy"},
// {"first": "Roger", "last": "Craig"}
// ]
// }
// "name.last" >> "Anderson"
// "age" >> 37
// "children" >> ["Sara","Alex","Jack"]
// "children.#" >> 3
// "children.1" >> "Alex"
// "child*.2" >> "Jack"
// "c?ildren.0" >> "Sara"
// "friends.#.first" >> ["James","Roger"]
//
// This function expects that the json is well-formed, and does not validate.
// Invalid json will not panic, but it may return back unexpected results.
Expand Down Expand Up @@ -2126,8 +2193,7 @@ func unescape(json string) string {
// The caseSensitive paramater is used when the tokens are Strings.
// The order when comparing two different type is:
//
// Null < False < Number < String < True < JSON
//
// Null < False < Number < String < True < JSON
func (t Result) Less(token Result, caseSensitive bool) bool {
if t.Type < token.Type {
return true
Expand Down Expand Up @@ -2556,25 +2622,23 @@ func validnull(data []byte, i int) (outi int, ok bool) {

// Valid returns true if the input is valid json.
//
// if !gjson.Valid(json) {
// return errors.New("invalid json")
// }
// value := gjson.Get(json, "name.last")
//
// if !gjson.Valid(json) {
// return errors.New("invalid json")
// }
// value := gjson.Get(json, "name.last")
func Valid(json string) bool {
_, ok := validpayload(stringBytes(json), 0)
return ok
}

// ValidBytes returns true if the input is valid json.
//
// if !gjson.Valid(json) {
// return errors.New("invalid json")
// }
// value := gjson.Get(json, "name.last")
// if !gjson.Valid(json) {
// return errors.New("invalid json")
// }
// value := gjson.Get(json, "name.last")
//
// If working with bytes, this method preferred over ValidBytes(string(data))
//
func ValidBytes(json []byte) bool {
_, ok := validpayload(json, 0)
return ok
Expand Down Expand Up @@ -2848,9 +2912,13 @@ func modReverse(json, arg string) string {
}

// @flatten an array with child arrays.
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,[6,7]]
//
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,[6,7]]
//
// The {"deep":true} arg can be provide for deep flattening.
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,6,7]
//
// [1,[2],[3,4],[5,[6,7]]] -> [1,2,3,4,5,6,7]
//
// The original json is returned when the json is not an array.
func modFlatten(json, arg string) string {
res := Parse(json)
Expand Down Expand Up @@ -2895,7 +2963,8 @@ func modFlatten(json, arg string) string {
}

// @keys extracts the keys from an object.
// {"first":"Tom","last":"Smith"} -> ["first","last"]
//
// {"first":"Tom","last":"Smith"} -> ["first","last"]
func modKeys(json, arg string) string {
v := Parse(json)
if !v.Exists() {
Expand All @@ -2922,7 +2991,8 @@ func modKeys(json, arg string) string {
}

// @values extracts the values from an object.
// {"first":"Tom","last":"Smith"} -> ["Tom","Smith"]
//
// {"first":"Tom","last":"Smith"} -> ["Tom","Smith"]
func modValues(json, arg string) string {
v := Parse(json)
if !v.Exists() {
Expand All @@ -2947,11 +3017,17 @@ func modValues(json, arg string) string {
}

// @join multiple objects into a single object.
// [{"first":"Tom"},{"last":"Smith"}] -> {"first","Tom","last":"Smith"}
//
// [{"first":"Tom"},{"last":"Smith"}] -> {"first","Tom","last":"Smith"}
//
// The arg can be "true" to specify that duplicate keys should be preserved.
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":37,"age":41}
//
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":37,"age":41}
//
// Without preserved keys:
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":41}
//
// [{"first":"Tom","age":37},{"age":41}] -> {"first","Tom","age":41}
//
// The original json is returned when the json is not an object.
func modJoin(json, arg string) string {
res := Parse(json)
Expand Down Expand Up @@ -3024,7 +3100,8 @@ func modValid(json, arg string) string {
}

// @fromstr converts a string to json
// "{\"id\":1023,\"name\":\"alert\"}" -> {"id":1023,"name":"alert"}
//
// "{\"id\":1023,\"name\":\"alert\"}" -> {"id":1023,"name":"alert"}
func modFromStr(json, arg string) string {
if !Valid(json) {
return ""
Expand All @@ -3033,7 +3110,8 @@ func modFromStr(json, arg string) string {
}

// @tostr converts a string to json
// {"id":1023,"name":"alert"} -> "{\"id\":1023,\"name\":\"alert\"}"
//
// {"id":1023,"name":"alert"} -> "{\"id\":1023,\"name\":\"alert\"}"
func modToStr(str, arg string) string {
return string(AppendJSONString(nil, str))
}
Expand Down Expand Up @@ -3210,11 +3288,11 @@ func revSquash(json string) string {
// Paths returns the original GJSON paths for a Result where the Result came
// from a simple query path that returns an array, like:
//
// gjson.Get(json, "friends.#.first")
// gjson.Get(json, "friends.#.first")
//
// The returned value will be in the form of a JSON array:
//
// ["friends.0.first","friends.1.first","friends.2.first"]
// ["friends.0.first","friends.1.first","friends.2.first"]
//
// The param 'json' must be the original JSON used when calling Get.
//
Expand All @@ -3239,11 +3317,11 @@ func (t Result) Paths(json string) []string {
// Path returns the original GJSON path for a Result where the Result came
// from a simple path that returns a single value, like:
//
// gjson.Get(json, "friends.#(last=Murphy)")
// gjson.Get(json, "friends.#(last=Murphy)")
//
// The returned value will be in the form of a JSON string:
//
// "friends.0"
// "friends.0"
//
// The param 'json' must be the original JSON used when calling Get.
//
Expand Down

0 comments on commit 8d2c36f

Please sign in to comment.