Added new tilde types and fixed ~false

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
This commit is contained in:
tidwall 2023-07-27 06:23:49 -07:00
parent e14b8d3708
commit 8d2c36ffa4
3 changed files with 208 additions and 68 deletions

View File

@ -137,12 +137,21 @@ next major release.*
The `~` (tilde) operator will convert a value to a boolean before comparison. 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: For example, using the following JSON:
```json ```json
{ {
"vals": [ "vals": [
{ "a": 1, "b": true }, { "a": 1, "b": "data" },
{ "a": 2, "b": true }, { "a": 2, "b": true },
{ "a": 3, "b": false }, { "a": 3, "b": false },
{ "a": 4, "b": "0" }, { "a": 4, "b": "0" },
@ -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] vals.#(b==~false)#.a >> [3,4,5,9,10,11]
``` ```
The last value which was non-existent is treated as `false` 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 ### Dot vs Pipe
The `.` is standard separator, but it's also possible to use a `|`. The `.` is standard separator, but it's also possible to use a `|`.

View File

@ -661,7 +661,6 @@ func (t Result) Exists() bool {
// nil, for JSON null // nil, for JSON null
// map[string]interface{}, for JSON objects // map[string]interface{}, for JSON objects
// []interface{}, for JSON arrays // []interface{}, for JSON arrays
//
func (t Result) Value() interface{} { func (t Result) Value() interface{} {
if t.Type == String { if t.Type == String {
return t.Str return t.Str
@ -826,16 +825,25 @@ func parseArrayPath(path string) (r arrayPathResult) {
} }
// splitQuery takes a query and splits it into three parts: // splitQuery takes a query and splits it into three parts:
//
// path, op, middle, and right. // path, op, middle, and right.
//
// So for this query: // So for this query:
//
// #(first_name=="Murphy").last // #(first_name=="Murphy").last
//
// Becomes // Becomes
//
// first_name # path // first_name # path
// =="Murphy" # middle // =="Murphy" # middle
// .last # right // .last # right
//
// Or, // Or,
//
// #(service_roles.#(=="one")).cap // #(service_roles.#(=="one")).cap
//
// Becomes // Becomes
//
// service_roles.#(=="one") # path // service_roles.#(=="one") # path
// # middle // # middle
// .cap # right // .cap # right
@ -1251,16 +1259,75 @@ func matchLimit(str, pattern string) bool {
return matched 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 { func queryMatches(rp *arrayPathResult, value Result) bool {
rpv := rp.query.value rpv := rp.query.value
if len(rpv) > 0 && rpv[0] == '~' { if len(rpv) > 0 {
if rpv[0] == '~' {
// convert to bool // convert to bool
rpv = rpv[1:] rpv = rpv[1:]
if value.Bool() { 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} value = Result{Type: True}
} else { } else {
value = Result{Type: False} value = Result{Type: False}
} }
} else {
rpv = ""
value = Result{}
}
}
} }
if !value.Exists() { if !value.Exists() {
return false return false
@ -2127,7 +2194,6 @@ func unescape(json string) string {
// The order when comparing two different type is: // 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 { func (t Result) Less(token Result, caseSensitive bool) bool {
if t.Type < token.Type { if t.Type < token.Type {
return true return true
@ -2560,7 +2626,6 @@ func validnull(data []byte, i int) (outi int, ok bool) {
// return errors.New("invalid json") // return errors.New("invalid json")
// } // }
// value := gjson.Get(json, "name.last") // value := gjson.Get(json, "name.last")
//
func Valid(json string) bool { func Valid(json string) bool {
_, ok := validpayload(stringBytes(json), 0) _, ok := validpayload(stringBytes(json), 0)
return ok return ok
@ -2574,7 +2639,6 @@ func Valid(json string) bool {
// value := gjson.Get(json, "name.last") // value := gjson.Get(json, "name.last")
// //
// If working with bytes, this method preferred over ValidBytes(string(data)) // If working with bytes, this method preferred over ValidBytes(string(data))
//
func ValidBytes(json []byte) bool { func ValidBytes(json []byte) bool {
_, ok := validpayload(json, 0) _, ok := validpayload(json, 0)
return ok return ok
@ -2848,9 +2912,13 @@ func modReverse(json, arg string) string {
} }
// @flatten an array with child arrays. // @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. // 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. // The original json is returned when the json is not an array.
func modFlatten(json, arg string) string { func modFlatten(json, arg string) string {
res := Parse(json) res := Parse(json)
@ -2895,6 +2963,7 @@ func modFlatten(json, arg string) string {
} }
// @keys extracts the keys from an object. // @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 { func modKeys(json, arg string) string {
v := Parse(json) v := Parse(json)
@ -2922,6 +2991,7 @@ func modKeys(json, arg string) string {
} }
// @values extracts the values from an object. // @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 { func modValues(json, arg string) string {
v := Parse(json) v := Parse(json)
@ -2947,11 +3017,17 @@ func modValues(json, arg string) string {
} }
// @join multiple objects into a single object. // @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. // 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: // 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. // The original json is returned when the json is not an object.
func modJoin(json, arg string) string { func modJoin(json, arg string) string {
res := Parse(json) res := Parse(json)
@ -3024,6 +3100,7 @@ func modValid(json, arg string) string {
} }
// @fromstr converts a string to json // @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 { func modFromStr(json, arg string) string {
if !Valid(json) { if !Valid(json) {
@ -3033,6 +3110,7 @@ func modFromStr(json, arg string) string {
} }
// @tostr converts a string to json // @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 { func modToStr(str, arg string) string {
return string(AppendJSONString(nil, str)) return string(AppendJSONString(nil, str))

View File

@ -2129,10 +2129,10 @@ func TestEncodedQueryString(t *testing.T) {
assert(t, Get(json, `friends.#(last=="Murphy").age`).Int() == 47) assert(t, Get(json, `friends.#(last=="Murphy").age`).Int() == 47)
} }
func TestBoolConvertQuery(t *testing.T) { func TestTildeQueries(t *testing.T) {
json := `{ json := `{
"vals": [ "vals": [
{ "a": 1, "b": true }, { "a": 1, "b": "data" },
{ "a": 2, "b": true }, { "a": 2, "b": true },
{ "a": 3, "b": false }, { "a": 3, "b": false },
{ "a": 4, "b": "0" }, { "a": 4, "b": "0" },
@ -2146,9 +2146,54 @@ func TestBoolConvertQuery(t *testing.T) {
] ]
}` }`
trues := Get(json, `vals.#(b==~true)#.a`).Raw trues := Get(json, `vals.#(b==~true)#.a`).Raw
truesNOT := Get(json, `vals.#(b!=~true)#.a`).Raw
falses := Get(json, `vals.#(b==~false)#.a`).Raw falses := Get(json, `vals.#(b==~false)#.a`).Raw
assert(t, trues == "[1,2,6,7,8]") falsesNOT := Get(json, `vals.#(b!=~false)#.a`).Raw
nulls := Get(json, `vals.#(b==~null)#.a`).Raw
nullsNOT := Get(json, `vals.#(b!=~null)#.a`).Raw
exists := Get(json, `vals.#(b==~*)#.a`).Raw
existsNOT := Get(json, `vals.#(b!=~*)#.a`).Raw
assert(t, trues == "[2,6,7,8]")
assert(t, truesNOT == "[1,3,4,5,9,10,11]")
assert(t, falses == "[3,4,5,9,10,11]") assert(t, falses == "[3,4,5,9,10,11]")
assert(t, falsesNOT == "[1,2,6,7,8]")
assert(t, nulls == "[10,11]")
assert(t, nullsNOT == "[1,2,3,4,5,6,7,8,9]")
assert(t, exists == "[1,2,3,4,5,6,7,8,9,10]")
assert(t, existsNOT == "[11]")
json = `{
"vals": [
{ "a": 1, "b": "something" },
{ "a": 2, "b": "else" },
{ "a": 3, "b": false },
{ "a": 4, "b": "0" },
{ "a": 5, "b": 0 },
{ "a": 6, "b": "1" },
{ "a": 7, "b": 1 },
{ "a": 8, "b": "true" },
{ "a": 9, "b": false },
{ "a": 10, "b": null },
{ "a": 11 }
],
"anything": "else"
}`
trues = Get(json, `vals.#(b==~true)#.a`).Raw
truesNOT = Get(json, `vals.#(b!=~true)#.a`).Raw
falses = Get(json, `vals.#(b==~false)#.a`).Raw
falsesNOT = Get(json, `vals.#(b!=~false)#.a`).Raw
nulls = Get(json, `vals.#(b==~null)#.a`).Raw
nullsNOT = Get(json, `vals.#(b!=~null)#.a`).Raw
exists = Get(json, `vals.#(b==~*)#.a`).Raw
existsNOT = Get(json, `vals.#(b!=~*)#.a`).Raw
assert(t, trues == "[6,7,8]")
assert(t, truesNOT == "[1,2,3,4,5,9,10,11]")
assert(t, falses == "[3,4,5,9,10,11]")
assert(t, falsesNOT == "[1,2,6,7,8]")
assert(t, nulls == "[10,11]")
assert(t, nullsNOT == "[1,2,3,4,5,6,7,8,9]")
assert(t, exists == "[1,2,3,4,5,6,7,8,9,10]")
assert(t, existsNOT == "[11]")
} }
func TestModifierDoubleQuotes(t *testing.T) { func TestModifierDoubleQuotes(t *testing.T) {