Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 9 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,24 +196,15 @@ For maps, the keys will be sorted into alphabetical order and they will be used

For strings instead of returning an array of characters instead will return a substring. For example if you applied `[0:3]` to the string `string` it would return `str`.

## Supported standard evaluation operations

| symbol | name | supported types | example | notes |
| --- | --- | --- | --- | --- |
| == | equals | any | 1 == 1 returns true | |
| != | not equals | any | 1 != 2 returns true | |
| * | multiplication | int\|float | 2*2 returns 4 | |
| / | division | int\|float | 10/5 returns 2 | if you supply two whole numbers you will only get a whole number response, even if there is a remainder i.e. 10/4 would return 2, not 2.5. to include remainders you would need to have the numerator as a float i.e. 10.0/4 would return 2.5 |
| + | addition | int\|float | 2+2 returns 4 | |
| - | subtraction | int\|float | 2-2 returns 0 | |
| % | remainder | int\|float | 5 % 2 returns 1 | this operator will divide the numerator by the denominator and then return the remainder |
| > | greater than | int\|float | 1 > 0 returns true | |
| >= | greater than or equal to | int\|float | 1 >= 1 returns true | |
| < | less than | int\|float | 1 < 2 returns true | |
| <= | less than or equal to | int\|float | 1 <= 1 returns true | |
| && | combine and | expression\|bool | true&&false returns false | evaluate two expressions that return true or false, and return true if both are true |
| \|\| | combine or | expression\|bool | true\|\|false returns true | evaluate two expressions that return true or false, and return true if either are true |
| (...) | sub-expression | expression | (1+2)*3 returns 9 | allows you to isolate a sub-expression so it will be evaluated first separate from the rest of the expression |
## Script Engine

The library supports scripts and filters using a [standard script engine](script/standard/README.md) included with this library.

Additionally, a custom script engine can be created and passed as an additional option when compiling the JSONPath selector

```golang

```

## History

Expand Down
108 changes: 108 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package jsonpath

import (
"github.com/evilmonkeyinc/jsonpath/option"
"github.com/evilmonkeyinc/jsonpath/script"
)

type sampleData struct {
Expensive float64 `json:"expensive"`
Store *storeData `json:"store"`
}

type storeData struct {
Book []*bookData `json:"book"`
}

type bookData struct {
Author string `json:"author"`
Category string `json:"category"`
ISBN string `json:"isbn"`
Price float64 `json:"price"`
Title string `json:"title"`
}

var sampleDataObject *sampleData = &sampleData{
Expensive: 10,
Store: &storeData{
Book: []*bookData{
{
Category: "reference",
Author: "Nigel Rees",
Title: "Sayings of the Century",
Price: 8.95,
},
{
Category: "fiction",
Author: "Evelyn Waugh",
Title: "Sword of Honour",
Price: 12.99,
},
{
Category: "fiction",
Author: "Herman Melville",
Title: "Moby Dick",
ISBN: "0-553-21311-3",
Price: 8.99,
},
{
Category: "fiction",
Author: "J. R. R. Tolkien",
Title: "The Lord of the Rings",
ISBN: "0-395-19395-8",
Price: 22.99,
},
},
},
}

var sampleDataString string = `
{
"store": {
"book": [{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
},
"expensive": 10
}
`

type testScriptEngine struct {
value interface{}
}

func (engine *testScriptEngine) Compile(expression string, options *option.QueryOptions) (script.CompiledExpression, error) {
return nil, nil
}

func (engine *testScriptEngine) Evaluate(root, current interface{}, expression string, options *option.QueryOptions) (interface{}, error) {
return nil, nil
}
143 changes: 26 additions & 117 deletions jsonpath.go
Original file line number Diff line number Diff line change
@@ -1,150 +1,59 @@
package jsonpath

import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/evilmonkeyinc/jsonpath/option"
"github.com/evilmonkeyinc/jsonpath/script"
"github.com/evilmonkeyinc/jsonpath/script/standard"
"github.com/evilmonkeyinc/jsonpath/token"
)

// Compile will compile the JSONPath selector
func Compile(selector string) (*Selector, error) {
engine := new(standard.ScriptEngine)

if selector == "$[?(@.key<3),?(@.key>6)]" {
// TODO
selector = "$[?(@.key<3),?(@.key>6)]"
func Compile(selector string, options ...Option) (*Selector, error) {
jsonPath := &Selector{
selector: selector,
}

jsonPath := &Selector{}
if err := jsonPath.compile(selector, engine); err != nil {
return nil, getInvalidJSONPathSelectorWithReason(selector, err)
for _, option := range options {
if err := option.Apply(jsonPath); err != nil {
return nil, err
}
}

return jsonPath, nil
}

// Query will return the result of the JSONPath selector applied against the specified JSON data.
func Query(selector string, jsonData interface{}) (interface{}, error) {
jsonPath, err := Compile(selector)
if err != nil {
return nil, getInvalidJSONPathSelectorWithReason(selector, err)
// Set defaults if options were not used
if jsonPath.engine == nil {
jsonPath.engine = new(standard.ScriptEngine)
}
return jsonPath.Query(jsonData)
}

// QueryString will return the result of the JSONPath selector applied against the specified JSON data.
func QueryString(selector string, jsonData string) (interface{}, error) {

jsonPath, err := Compile(selector)
tokenStrings, err := token.Tokenize(jsonPath.selector)
if err != nil {
return nil, getInvalidJSONPathSelectorWithReason(selector, err)
}
return jsonPath.QueryString(jsonData)
}

// Selector represents a compiled JSONPath selector
// and exposes functions to query JSON data and objects.
type Selector struct {
Options *option.QueryOptions
engine script.Engine
tokens []token.Token
selector string
}

// String returns the compiled selector string representation
func (query *Selector) String() string {
jsonPath := ""
for _, token := range query.tokens {
jsonPath += fmt.Sprintf("%s", token)
}
return jsonPath
}

func (query *Selector) compile(selector string, engine script.Engine) error {
query.engine = engine
query.selector = selector

tokenStrings, err := token.Tokenize(selector)
if err != nil {
return err
}

tokens := make([]token.Token, len(tokenStrings))
for idx, tokenString := range tokenStrings {
token, err := token.Parse(tokenString, query.engine, query.Options)
token, err := token.Parse(tokenString, jsonPath.engine, jsonPath.Options)
if err != nil {
return err
return nil, getInvalidJSONPathSelectorWithReason(selector, err)
}
tokens[idx] = token
}
query.tokens = tokens
jsonPath.tokens = tokens

return nil
return jsonPath, nil
}

// Query will return the result of the JSONPath query applied against the specified JSON data.
func (query *Selector) Query(root interface{}) (interface{}, error) {
if len(query.tokens) == 0 {
return nil, getInvalidJSONPathSelector(query.selector)
}

tokens := make([]token.Token, 0)
if len(query.tokens) > 1 {
tokens = query.tokens[1:]
}

found, err := query.tokens[0].Apply(root, root, tokens)
// Query will return the result of the JSONPath selector applied against the specified JSON data.
func Query(selector string, jsonData interface{}, options ...Option) (interface{}, error) {
jsonPath, err := Compile(selector, options...)
if err != nil {
return nil, err
return nil, getInvalidJSONPathSelectorWithReason(selector, err)
}
return found, nil
return jsonPath.Query(jsonData)
}

// QueryString will return the result of the JSONPath query applied against the specified JSON data.
func (query *Selector) QueryString(jsonData string) (interface{}, error) {
jsonData = strings.TrimSpace(jsonData)
if jsonData == "" {
return nil, getInvalidJSONData(errDataIsUnexpectedTypeOrNil)
}

var root interface{}

if strings.HasPrefix(jsonData, "{") && strings.HasSuffix(jsonData, "}") {
// object
root = make(map[string]interface{})
if err := json.Unmarshal([]byte(jsonData), &root); err != nil {
return nil, getInvalidJSONData(err)
}
} else if strings.HasPrefix(jsonData, "[") && strings.HasSuffix(jsonData, "]") {
// array
root = make([]interface{}, 0)
if err := json.Unmarshal([]byte(jsonData), &root); err != nil {
return nil, getInvalidJSONData(err)
}
} else if len(jsonData) > 2 && strings.HasPrefix(jsonData, "\"") && strings.HasPrefix(jsonData, "\"") {
// string
root = jsonData[1 : len(jsonData)-1]
} else if strings.ToLower(jsonData) == "true" {
// bool true
root = true
} else if strings.ToLower(jsonData) == "false" {
// bool false
root = false
} else if val, err := strconv.ParseInt(jsonData, 10, 64); err == nil {
// integer
root = val
} else if val, err := strconv.ParseFloat(jsonData, 64); err == nil {
// float
root = val
} else {
return nil, getInvalidJSONData(errDataIsUnexpectedTypeOrNil)
// QueryString will return the result of the JSONPath selector applied against the specified JSON data.
func QueryString(selector string, jsonData string, options ...Option) (interface{}, error) {
jsonPath, err := Compile(selector, options...)
if err != nil {
return nil, getInvalidJSONPathSelectorWithReason(selector, err)
}

return query.Query(root)
return jsonPath.QueryString(jsonData)
}
Loading