diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml new file mode 100644 index 0000000..f311ebe --- /dev/null +++ b/.github/workflows/testing.yaml @@ -0,0 +1,23 @@ +name: Run Go tests +on: [ push ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.x' + + - name: Install dependencies + run: go get . + + - name: Build + run: go build + + - name: Test with the Go CLI + run: go test \ No newline at end of file diff --git a/README.md b/README.md index 657e255..cc76d23 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ A traefik Plugin for securing the upstream service with OpenID Connect acting as a relying party. -> [!NOTE] -> This document always represents the latest version, which may not have been released yet. +> [!NOTE] +> This document always represents the latest version, which may not have been released yet. > Therefore, some features may not be available currently but will be available soon. > You can use the GIT-Tags to check individual versions. @@ -51,14 +51,18 @@ http: Authorization: AssertClaims: - Name: "preferred_username" - Values: "alice@gmail.com,bob@gmail.com" + AnyOf: ["alice@gmail.com", "bob@gmail.com"] + - Name: "roles" + AllOf: ["admin", "media"] + - Name: "user.first_name" + AnyOf: ["Alice"] Headers: MapClaims: - Claim: "preferred_username" Header: "X-Oidc-Username" - Claim: "sub" Header: "X-Oidc-Subject" - + routers: whoami: entryPoints: ["web"] @@ -110,11 +114,69 @@ http: ### ClaimAssertion Block +If only the `Name` property is set and no additional assertions are defined it is only checked whether there exist any matches for the name of this claim without any verification on their values. +Additionaly, the `Name` field can be any [json path](https://jsonpath.com/). The `Name` gets prefixed with `$.` to match from the root element. The usage of json paths allows for assertions on deeply nested json structures. + | Name | Required | Type | Default | Description | |---|---|---|---|---| | Name | yes | `string` | *none* | The name of the claim in the access token. | -| Value | no | `string` | *none* | The required value of the claim. If *Value* and *Values* are not set, only the presence of the claim will be checked. | -| Values | no | `string[]` | *none* | An array of allowed strings. The user is authorized if the claim matched any of these. | +| AnyOf | no | `string[]` | *none* | An array of allowed strings. The user is authorized if any value matching the name of the claim contains (or is) a value of this array. | +| AllOf | no | `string[]` | *none* | An array of required strings. The user is only authorized if any value matching the name of the claim contains (or is) a value of this array and all values of this array are covered in the end. | + +It is possible to combine `AnyOf` and `AllOf` quantifiers for one assertion + +
+ + Examples + + All of the examples below work on this json structure: + + ```json + { + "store": { + "bicycle": { + "color": "red", + "price": 19.95 + }, + "book": [ + { + "author": "Herman Melville", + "category": "fiction", + "isbn": "0-553-21311-3", + "price": 8.99, + "title": "Moby Dick" + }, + { + "author": "J. R. R. Tolkien", + "category": "fiction", + "isbn": "0-395-19395-8", + "price": 22.99, + "title": "The Lord of the Rings" + } + ], + } + } + ``` + + **Example**: Expect array to contain a set of values + ```yaml + Name: store.book[*].price + AllOf: [ 22.99, 8.99 ] + ``` + This assertion would succeed as the `book` array contains all values specified by the `AllOf` quantifier + ```yaml + Name: store.book[*].price + AllOf: [ 22.99, 8.99, 1 ] + ``` + This assertion would fail as the `book` array contains no entry for which the `price` is `1` + + **Example**: Expect object key to be any value of a set of values + ```yaml + Name: store.bicycle.color + AnyOf: [ "red", "blue", "green" ] + ``` + This assertion would succeed as the `store` object contains a `bicycle` object whose `color` is `red` +
### Headers Block @@ -141,5 +203,5 @@ CLIENT_SECRET=... The run `docker compose up` to run traefik locally. -Now browse to http://localhost:9080. You should be redirected to your IDP. +Now browse to http://localhost:9080. You should be redirected to your IDP. After you've logged in, you should be redirected back to http://localhost:9080 and see a WHOAMI page. diff --git a/authorization.go b/authorization.go new file mode 100644 index 0000000..8c4597a --- /dev/null +++ b/authorization.go @@ -0,0 +1,131 @@ +package traefik_oidc_auth + +import ( + "encoding/json" + "fmt" + "slices" + "strings" + + "github.com/golang-jwt/jwt/v5" + "github.com/spyzhov/ajson" +) + +func (toa *TraefikOidcAuth) isAuthorized(claims *jwt.MapClaims) bool { + authorization := toa.Config.Authorization + + if authorization.AssertClaims != nil && len(authorization.AssertClaims) > 0 { + parsed, err := json.Marshal(*claims) + if err != nil { + log(toa.Config.LogLevel, LogLevelWarn, "Error whilst marshalling claims object: %s", err.Error()) + return false + } + + assertions: + for _, assertion := range authorization.AssertClaims { + value, err := ajson.JSONPath(parsed, fmt.Sprintf("$.%s", assertion.Name)) + if err != nil { + log(toa.Config.LogLevel, LogLevelWarn, "Error whilst parsing path for claim %s in token claims: %s", assertion.Name, err.Error()) + return false + } else if len(value) == 0 { + log(toa.Config.LogLevel, LogLevelWarn, "Unauthorized. Unable to find claim %s in token claims.", assertion.Name) + return false + } + + if len(assertion.AllOf) == 0 && len(assertion.AnyOf) == 0 { + log(toa.Config.LogLevel, LogLevelDebug, "Authorized claim %s. No assertions were defined and claim exists", assertion.Name) + continue assertions + } + + // check all matched nodes whether for one of the nodes all assertions hold + // should the assertions hold for no node we return `false` to indicate + // an unauthorized state + + allMatches := make([]bool, len(assertion.AllOf)) + anyMatch := false + + matches: + for _, val := range value { + unpacked, err := val.Unpack() + if err != nil { + log(toa.Config.LogLevel, LogLevelError, "Error whilst unpacking json node: %s", err.Error()) + continue matches + } + + switch val := unpacked.(type) { + // the value is any array + case []interface{}: + mapped := make([]string, len(val)) + for i, rawVal := range val { + mapped[i] = fmt.Sprintf("%v", rawVal) + } + + // first check whether allOf assertion is fulfilled -> return false if not + if len(assertion.AllOf) > 0 { + for _, assert := range assertion.AllOf { + if !slices.Contains(mapped, assert) { + break matches + } + } + } + // should allOf assertion be fulfilled check whether anyOf assertion is fulfilled -> return true when fulfilled + if len(assertion.AnyOf) > 0 { + for _, assert := range assertion.AnyOf { + if slices.Contains(mapped, assert) { + log(toa.Config.LogLevel, LogLevelDebug, "Authorized claim %s: Found value %s which is any of [%s]", assertion.Name, assert, strings.Join(assertion.AnyOf, ", ")) + continue assertions + } + } + continue matches + } + log(toa.Config.LogLevel, LogLevelDebug, "Authorized claim %s: Found all values of [%s]", assertion.Name, strings.Join(assertion.AllOf, ", ")) + continue assertions + // the value is any other json type + default: + strVal := fmt.Sprintf("%v", val) + if len(assertion.AnyOf) > 0 { + if slices.Contains(assertion.AnyOf, strVal) { + anyMatch = true + } + } + if len(assertion.AllOf) > 0 { + for i, assert := range assertion.AllOf { + if assert == strVal { + allMatches[i] = true + break + } + } + } + continue matches + } + } + + if len(assertion.AnyOf) > 0 && anyMatch && len(assertion.AllOf) > 0 && !slices.Contains(allMatches, false) { + log(toa.Config.LogLevel, LogLevelDebug, "Authorized claim %s: Found any value of [%s] and all values of [%s]", assertion.Name, strings.Join(assertion.AnyOf, ", "), strings.Join(assertion.AllOf, ", ")) + continue assertions + } else if len(assertion.AnyOf) > 0 && anyMatch && len(assertion.AllOf) == 0 { + log(toa.Config.LogLevel, LogLevelDebug, "Authorized claim %s: Found any value of [%s]", assertion.Name, strings.Join(assertion.AnyOf, ", ")) + continue assertions + } else if len(assertion.AllOf) > 0 && !slices.Contains(allMatches, false) && len(assertion.AnyOf) == 0 { + log(toa.Config.LogLevel, LogLevelDebug, "Authorized claim %s: Found all values of [%s]", assertion.Name, strings.Join(assertion.AllOf, ", ")) + continue assertions + } + + if len(assertion.AllOf) > 0 && len(assertion.AnyOf) > 0 { + log(toa.Config.LogLevel, LogLevelWarn, "Unauthorized. Expected claim %s to contain any value of [%s] and all values of [%s]", assertion.Name, strings.Join(assertion.AnyOf, ", "), strings.Join(assertion.AllOf, ", ")) + } else if len(assertion.AllOf) > 0 { + log(toa.Config.LogLevel, LogLevelWarn, "Unauthorized. Expected claim %s to contain all values of [%s]", assertion.Name, strings.Join(assertion.AllOf, ", ")) + } else if len(assertion.AnyOf) > 0 { + log(toa.Config.LogLevel, LogLevelWarn, "Unauthorized. Expected claim %s to contain any value of [%s]", assertion.Name, strings.Join(assertion.AnyOf, ", ")) + } + + log(toa.Config.LogLevel, LogLevelDebug, "Available claims are:") + for key, val := range *claims { + log(toa.Config.LogLevel, LogLevelDebug, " %v = %v", key, val) + } + + return false + } + } + + return true +} diff --git a/authorization_test.go b/authorization_test.go new file mode 100644 index 0000000..745c44e --- /dev/null +++ b/authorization_test.go @@ -0,0 +1,230 @@ +package traefik_oidc_auth + +import ( + "encoding/json" + "testing" + + "github.com/golang-jwt/jwt/v5" +) + +func createAuthInstance(claims []ClaimAssertion) TraefikOidcAuth { + return TraefikOidcAuth{ + Config: &Config{ + Authorization: &AuthorizationConfig{ + AssertClaims: claims, + }, + }, + } +} + +func getTestClaims() *jwt.MapClaims { + bytes := []byte(`{ + "name": "Alice", + "age": 67, + "children": [ + { "name": "Bob", "age": 25 }, + { "name": "Eve", "age": 22 } + ], + "roles": [ + "support", + "accountant", + "administrator" + ], + "address": { + "country": "USA", + "street": "Freedom Rd.", + "neighbours": [ + "Joe", + "Sam" + ] + } + }`) + + claims := jwt.MapClaims{} + err := json.Unmarshal(bytes, &claims) + if err != nil { + panic(err) + } + return &claims +} + +func TestClaimNameExists(t *testing.T) { + claims := getTestClaims() + toa := createAuthInstance([]ClaimAssertion { + { Name: "name" }, + }) + + if !toa.isAuthorized(claims) { + t.Fatal("Should authorize as a claim with the provided name exists") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "names" }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize as no claim with the provided name exists") + } +} + +func TestSimpleAssertions(t *testing.T) { + claims := getTestClaims() + toa := createAuthInstance([]ClaimAssertion { + { Name: "name", AnyOf: []string { "Alice", "Bob", "Bruno" } }, + }) + + if !toa.isAuthorized(claims) { + t.Fatal("Should authorize since value is any of the provided values") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "name", AnyOf: []string { "Ben", "Joe", "Sam" } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since value is none of the provided values") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "name", AllOf: []string { "Alice" } }, + }) + + if !toa.isAuthorized(claims) { + t.Fatal("Should authorize since the single value matches all of the provided values") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "name", AllOf: []string { "Alice", "Bob", "Bruno" } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since the single value match cannot contain all values of array") + } +} + +func TestNestedAssertions(t *testing.T) { + claims := getTestClaims() + toa := createAuthInstance([]ClaimAssertion { + { Name: "address.street", AnyOf: []string { "Freedom Rd.", "Eagle St." } }, + }) + + if !toa.isAuthorized(claims) { + t.Fatal("Should authorize since nested value is any of the provided values") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "address.street", AnyOf: []string { "Concrete HWY" } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since nested value is none of the provided values") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "address.street", AllOf: []string { "Freedom Rd." } }, + }) + + if !toa.isAuthorized(claims) { + t.Fatal("Should authorize since the single value matches all of the provided values") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "address.street", AllOf: []string { "Freedom Rd.", "Eagle St." } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since the single value match cannot contain all values of array") + } +} + +func TestArrayAssertions(t *testing.T) { + claims := getTestClaims() + toa := createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AnyOf: []string { "Joe", "Bob", "Sam" } }, + }) + + if !toa.isAuthorized(claims) { + t.Fatal("Should authorize since some of the values are part of the provided values") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AnyOf: []string { "Joe", "Sam", "Alex" } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since values are none of the provided values") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AllOf: []string { "Bob", "Eve" } }, + }) + + if !toa.isAuthorized(claims) { + t.Fatal("Should authorize since all of the provided values have a matching claim value") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AllOf: []string { "Bob", "Eve", "Alex" } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since not all of the provided values have a matching claim value") + } +} + +func TestCombinedAssertions(t *testing.T) { + claims := getTestClaims() + toa := createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AnyOf: []string { "Joe", "Bob", "Sam" }, AllOf: []string { "Eve", "Bob" } }, + }) + + if !toa.isAuthorized(claims) { + t.Fatal("Should authorize since both assertion quantifiers have matching values") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AnyOf: []string { "Joe", "Bob", "Sam" }, AllOf: []string { "Eve", "Bob", "Alex" } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since not all values of the allOf quantifier are matched") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AnyOf: []string { "Sam" }, AllOf: []string { "Eve", "Bob" } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since no value of the anyOf quantifier is matched") + } +} + +func TestMultipleAssertions(t *testing.T) { + claims := getTestClaims() + toa := createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AnyOf: []string { "Joe", "Bob", "Sam" }, AllOf: []string { "Eve", "Bob" } }, + { Name: "name", AnyOf: []string { "Alice", "Alex" } }, + }) + + if !toa.isAuthorized(claims) { + t.Fatal("Should authorize since both assertions hold against the provided claims") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AnyOf: []string { "Joe", "Bob", "Sam" }, AllOf: []string { "Eve", "Bob", "Alex" } }, + { Name: "name", AnyOf: []string { "Alice", "Alex" } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since one of the assertions does not hold") + } + + toa = createAuthInstance([]ClaimAssertion { + { Name: "children[*].name", AnyOf: []string { "Joe", "Bob", "Sam" }, AllOf: []string { "Eve", "Bob", "Alex" } }, + { Name: "name", AnyOf: []string { "Alex", "Ben" } }, + }) + + if toa.isAuthorized(claims) { + t.Fatal("Should not authorize since both of the assertions do not hold") + } +} \ No newline at end of file diff --git a/config.go b/config.go index 8d88df3..a6c3418 100644 --- a/config.go +++ b/config.go @@ -72,9 +72,9 @@ type AuthorizationConfig struct { } type ClaimAssertion struct { - Name string `json:"name"` - Value string `json:"value"` - Values []string `json:"values"` + Name string `json:"name"` + AnyOf []string `json:"anyOf"` + AllOf []string `json:"allOf"` } type HeadersConfig struct { diff --git a/go.mod b/go.mod index 78f1e16..1a0292c 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/sevensolutions/traefik-oidc-auth go 1.21.4 -require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect +require ( + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/spyzhov/ajson v0.9.4 +) diff --git a/go.sum b/go.sum index f56d3e6..18e28b8 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/spyzhov/ajson v0.9.4 h1:MVibcTCgO7DY4IlskdqIlCmDOsUOZ9P7oKj8ifdcf84= +github.com/spyzhov/ajson v0.9.4/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= diff --git a/http.yml b/http.yml index 8d47783..2926e7f 100644 --- a/http.yml +++ b/http.yml @@ -23,6 +23,12 @@ http: Header: "X-Oidc-Username" - Claim: "sub" Header: "X-Oidc-Subject" + # Authorization: + # AssertClaims: + # - Name: roles + # AnyOf: ["admin", "media"] + # - Name: some.nested.key + # AnyOf: ["some value"] # If set, only the /login endpoint will initiate the login flow #LoginUri: "/login" #PostLoginRedirectUri: "/" diff --git a/main.go b/main.go index 25874fe..9641a04 100644 --- a/main.go +++ b/main.go @@ -8,11 +8,8 @@ import ( "io" "net/http" "net/url" - "slices" "strings" "time" - - "github.com/golang-jwt/jwt/v5" ) type TraefikOidcAuth struct { @@ -226,49 +223,6 @@ func (toa *TraefikOidcAuth) handleLogout(rw http.ResponseWriter, req *http.Reque http.Redirect(rw, req, endSessionURL.String(), http.StatusFound) } -func (toa *TraefikOidcAuth) isAuthorized(claims *jwt.MapClaims) bool { - authorization := toa.Config.Authorization - - if authorization.AssertClaims != nil && len(authorization.AssertClaims) > 0 { - for _, assertion := range authorization.AssertClaims { - found := false - isArray := assertion.Values != nil && len(assertion.Values) > 0 - - for key, val := range *claims { - strVal := fmt.Sprintf("%v", val) - if key == assertion.Name { - if isArray { - if slices.Contains(assertion.Values, strVal) { - found = true - break - } - } else if assertion.Value == "" || assertion.Value == strVal { - found = true - break - } - } - } - - if !found { - if isArray { - log(toa.Config.LogLevel, LogLevelWarn, "Unauthorized. Missing claim %s with value one of [%s].", assertion.Name, strings.Join(assertion.Values, ", ")) - } else { - log(toa.Config.LogLevel, LogLevelWarn, "Unauthorized. Missing claim %s with value %s.", assertion.Name, assertion.Value) - } - - log(toa.Config.LogLevel, LogLevelInfo, "Available claims are:") - for key, val := range *claims { - log(toa.Config.LogLevel, LogLevelInfo, " %v = %v", key, val) - } - - return false - } - } - } - - return true -} - func (toa *TraefikOidcAuth) handleUnauthorized(rw http.ResponseWriter, req *http.Request) { if toa.Config.LoginUri == "" { toa.redirectToProvider(rw, req) diff --git a/vendor/github.com/spyzhov/ajson/.gitignore b/vendor/github.com/spyzhov/ajson/.gitignore new file mode 100644 index 0000000..72bfb67 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/.gitignore @@ -0,0 +1,19 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# GoLand IDE= +.idea + +# Golang +vendor/ diff --git a/vendor/github.com/spyzhov/ajson/.golangci.yml b/vendor/github.com/spyzhov/ajson/.golangci.yml new file mode 100644 index 0000000..5dcf2f0 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/.golangci.yml @@ -0,0 +1,13 @@ +linters: + disable-all: true + enable: +# - gofmt # removed due to https://github.com/golangci/golangci-lint/issues/3229 + - govet + - errcheck + - staticcheck + - unused + - gosimple + - ineffassign + - typecheck + - gosec + - unused diff --git a/vendor/github.com/spyzhov/ajson/LICENSE b/vendor/github.com/spyzhov/ajson/LICENSE new file mode 100644 index 0000000..28bce57 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Pyzhov Stepan + +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. diff --git a/vendor/github.com/spyzhov/ajson/README.md b/vendor/github.com/spyzhov/ajson/README.md new file mode 100644 index 0000000..5f0adf0 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/README.md @@ -0,0 +1,645 @@ +# Abstract JSON + +[![Build](https://github.com/spyzhov/ajson/actions/workflows/main.yml/badge.svg)](https://github.com/spyzhov/ajson/actions/workflows/main.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/spyzhov/ajson)](https://goreportcard.com/report/github.com/spyzhov/ajson) +[![GoDoc](https://godoc.org/github.com/spyzhov/ajson?status.svg)](https://godoc.org/github.com/spyzhov/ajson) +[![codecov](https://codecov.io/gh/spyzhov/ajson/branch/master/graph/badge.svg)](https://codecov.io/gh/spyzhov/ajson) +[![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/avelino/awesome-go#json) + +Abstract [JSON](https://www.json.org/) is a small golang package provides a parser for JSON with support of JSONPath, in case when you are not sure in its structure. + +Method `Unmarshal` will scan all the byte slice to create a root node of JSON structure, with all its behaviors. + +Method `Marshal` will serialize current `Node` object to JSON structure. + +Each `Node` has its own type and calculated value, which will be calculated on demand. +Calculated value saves in `atomic.Value`, so it's thread safe. + +Method `JSONPath` will returns slice of found elements in current JSON data, by [JSONPath](http://goessner.net/articles/JsonPath/) request. + +## Compare with other solutions + +Check the [cburgmer/json-path-comparison](https://cburgmer.github.io/json-path-comparison/) project. + +# Usage + +[Playground](https://play.golang.com/p/iIxkktxN0SK) + +```go +package main + +import ( + "fmt" + "github.com/spyzhov/ajson" +) + +func main() { + json := []byte(`...`) + + root, _ := ajson.Unmarshal(json) + nodes, _ := root.JSONPath("$..price") + for _, node := range nodes { + node.SetNumeric(node.MustNumeric() * 1.25) + node.Parent().AppendObject("currency", ajson.StringNode("", "EUR")) + } + result, _ := ajson.Marshal(root) + + fmt.Printf("%s", result) +} +``` + +# Console application + +You can download `ajson` cli from the [release page](https://github.com/spyzhov/ajson/releases), or install from the source: + +```shell script +go get github.com/spyzhov/ajson/cmd/ajson@v0.9.4 +``` + +Usage: + +``` +Usage: ajson [-mq] "jsonpath" ["input"] + Read JSON and evaluate it with JSONPath. +Parameters: + -m, --multiline Input file/stream will be read as a multiline JSON. Each line should have a full valid JSON. + -q, --quiet Do not print errors into the STDERR. +Argument: + jsonpath Valid JSONPath or evaluate string (Examples: "$..[?(@.price)]", "$..price", "avg($..price)") + input Path to the JSON file. Leave it blank to use STDIN. +``` + +Examples: + +```shell script + ajson "avg($..registered.age)" "https://randomuser.me/api/?results=5000" + ajson "$.results.*.name" "https://randomuser.me/api/?results=10" + curl -s "https://randomuser.me/api/?results=10" | ajson "$..coordinates" + ajson "$" example.json + echo "3" | ajson "2 * pi * $" + docker logs image-name -f | ajson -qm 'root($[?(@=="ERROR" && key(@)=="severity")])' +``` + +# JSONPath + +Current package supports JSONPath selection described at [http://goessner.net/articles/JsonPath/](http://goessner.net/articles/JsonPath/). + +JSONPath expressions always refer to a JSON structure in the same way as XPath expression are used in combination with an XML document. +Since a JSON structure is usually anonymous and doesn't necessarily have a "root member object" JSONPath assumes the abstract name $ assigned to the outer level object. + +JSONPath expressions can use the dot–notation + +`$.store.book[0].title` + +or the bracket–notation + +`$['store']['book'][0]['title']` + +for input paths. Internal or output paths will always be converted to the more general bracket–notation. + +JSONPath allows the wildcard symbol `*` for member names and array indices. +It borrows the descendant operator `..` from E4X and the array slice syntax proposal `[start:end:step]` from ECMASCRIPT 4. + +Expressions of the underlying scripting language `()` can be used as an alternative to explicit names or indices as in + +`$.store.book[(@.length-1)].title` + +using the symbol `@` for the current object. Filter expressions are supported via the syntax `?()` as in + +`$.store.book[?(@.price < 10)].title` + +Here is a complete overview and a side by side comparison of the JSONPath syntax elements with its XPath counterparts. + +| JSONPath | Description | +|----------|---| +| `$` | the root object/element | +| `@` | the current object/element | +| `.` or `[]` | child operator | +| `..` | recursive descent. JSONPath borrows this syntax from E4X. | +| `*` | wildcard. All objects/elements regardless their names. | +| `[]` | subscript operator. XPath uses it to iterate over element collections and for predicates. In Javascript and JSON it is the native array operator. | +| `[,]` | Union operator in XPath results in a combination of node sets. JSONPath allows alternate names or array indices as a set. | +| `[start:end:step]` | array slice operator borrowed from ES4. | +| `?()` | applies a filter (script) expression. | +| `()` | script expression, using the underlying script engine. | + +## Script engine + +### Predefined constant + +Package has several predefined constants. + + e math.E float64 + pi math.Pi float64 + phi math.Phi float64 + + sqrt2 math.Sqrt2 float64 + sqrte math.SqrtE float64 + sqrtpi math.SqrtPi float64 + sqrtphi math.SqrtPhi float64 + + ln2 math.Ln2 float64 + log2e math.Log2E float64 + ln10 math.Ln10 float64 + log10e math.Log10E float64 + + true true bool + false false bool + null nil interface{} + +You are free to add new one with function `AddConstant`: + +```go + AddConstant("c", NumericNode("speed of light in vacuum", 299_792_458)) +``` + +#### Examples + +
+Using `true` in path + +[Playground](https://play.golang.org/p/h0oFLaE11Tn) + +```go +package main + +import ( + "fmt" + + "github.com/spyzhov/ajson" +) + +func main() { + json := []byte(`{"foo": [true, null, false, 1, "bar", true, 1e3], "bar": [true, "baz", false]}`) + result, _ := ajson.JSONPath(json, `$..[?(@ == true)]`) + fmt.Printf("Count of `true` values: %d", len(result)) +} +``` +Output: +``` +Count of `true` values: 3 +``` +
+
+Using `null` in eval + +[Playground](https://play.golang.org/p/wpqh1Fw5vWE) + +```go +package main + +import ( + "fmt" + + "github.com/spyzhov/ajson" +) + +func main() { + json := []byte(`{"foo": [true, null, false, 1, "bar", true, 1e3], "bar": [true, "baz", false]}`) + result, _ := ajson.JSONPath(json, `$..[?(@ == true)]`) + fmt.Printf("Count of `true` values: %d", len(result)) +} +``` +Output: +``` +Count of `true` values: 3 +``` +
+ +### Supported operations + +Package has several predefined operators. + +[Operator precedence](https://golang.org/ref/spec#Operator_precedence) + + Precedence Operator + 6 ** + 5 * / % << >> & &^ + 4 + - | ^ + 3 == != < <= > >= =~ + 2 && + 1 || + +[Arithmetic operators](https://golang.org/ref/spec#Arithmetic_operators) + + ** power integers, floats + + sum integers, floats, strings + - difference integers, floats + * product integers, floats + / quotient integers, floats + % remainder integers + + & bitwise AND integers + | bitwise OR integers + ^ bitwise XOR integers + &^ bit clear (AND NOT) integers + + << left shift integer << unsigned integer + >> right shift integer >> unsigned integer + + == equals any + != not equals any + < less any + <= less or equals any + > larger any + >= larger or equals any + =~ equals regex string strings + +You are free to add new one with function `AddOperation`: + +```go + AddOperation("<>", 3, false, func(left *ajson.Node, right *ajson.Node) (node *ajson.Node, err error) { + result, err := left.Eq(right) + if err != nil { + return nil, err + } + return BoolNode("neq", !result), nil + }) +``` + +#### Examples + +
+Using `regex` operator + +[Playground](https://play.golang.org/p/Lm_F4OGTMWl) + +```go +package main + +import ( + "fmt" + + "github.com/spyzhov/ajson" +) + +func main() { + json := []byte(`[{"name":"Foo","mail":"foo@example.com"},{"name":"bar","mail":"bar@example.org"}]`) + result, err := ajson.JSONPath(json, `$.[?(@.mail =~ '.+@example\\.com')]`) + if err != nil { + panic(err) + } + fmt.Printf("JSON: %s", result[0].Source()) + // Output: + // JSON: {"name":"Foo","mail":"foo@example.com"} +} + +``` +Output: +``` +JSON: {"name":"Foo","mail":"foo@example.com"} +``` +
+ +### Supported functions + +Package has several predefined functions. + + abs math.Abs integers, floats + acos math.Acos integers, floats + acosh math.Acosh integers, floats + asin math.Asin integers, floats + asinh math.Asinh integers, floats + atan math.Atan integers, floats + atanh math.Atanh integers, floats + avg Average array of integers or floats + b64decode b64 Decoding string + b64encode b64 Encoding string + b64encoden b64 Encoding (no padding) string + cbrt math.Cbrt integers, floats + ceil math.Ceil integers, floats + cos math.Cos integers, floats + cosh math.Cosh integers, floats + erf math.Erf integers, floats + erfc math.Erfc integers, floats + erfcinv math.Erfcinv integers, floats + erfinv math.Erfinv integers, floats + exp math.Exp integers, floats + exp2 math.Exp2 integers, floats + expm1 math.Expm1 integers, floats + factorial N! unsigned integer + first Get first element any + floor math.Floor integers, floats + gamma math.Gamma integers, floats + j0 math.J0 integers, floats + j1 math.J1 integers, floats + key Key of element string + last Get last element any + length Length of array array, string + log math.Log integers, floats + log10 math.Log10 integers, floats + log1p math.Log1p integers, floats + log2 math.Log2 integers, floats + logb math.Logb integers, floats + not not any + parent Get parent element any + pow10 math.Pow10 integer + rand N*rand.Float64 float + randint rand.Intn integer + root Get root element any + round math.Round integers, floats + roundtoeven math.RoundToEven integers, floats + sin math.Sin integers, floats + sinh math.Sinh integers, floats + size Count of elements array, object + sum Sum array of integers or floats + sqrt math.Sqrt integers, floats + tan math.Tan integers, floats + tanh math.Tanh integers, floats + trunc math.Trunc integers, floats + y0 math.Y0 integers, floats + y1 math.Y1 integers, floats + +You are free to add new one with function `AddFunction`: + +```go + AddFunction("trim", func(node *ajson.Node) (result *Node, err error) { + if node.IsString() { + return StringNode("trim", strings.TrimSpace(node.MustString())), nil + } + return + }) +``` + +#### Examples + +
+Using `avg` for array + +[Playground](https://play.golang.org/p/cM66hTE-CX1) + +```go +package main + +import ( + "fmt" + + "github.com/spyzhov/ajson" +) + +func main() { + json := []byte(`{"prices": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}`) + root, err := ajson.Unmarshal(json) + if err != nil { + panic(err) + } + result, err := ajson.Eval(root, `avg($.prices)`) + if err != nil { + panic(err) + } + fmt.Printf("Avg price: %0.1f", result.MustNumeric()) + // Output: + // Avg price: 5.5 +} +``` +Output: +``` +Avg price: 5.5 +``` +
+ +# Examples + +Calculating `AVG(price)` when object is heterogeneous. + +```json +{ + "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 + }, + "tools": null + } +} +``` + +## Unmarshal + +[Playground](https://play.golang.org/p/xny93dzjZCK) +```go +package main + +import ( + "fmt" + "github.com/spyzhov/ajson" +) + +func main() { + data := []byte(`{"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}, "tools": null}}`) + + root, err := ajson.Unmarshal(data) + if err != nil { + panic(err) + } + + store := root.MustKey("store").MustObject() + + var prices float64 + size := 0 + for _, objects := range store { + if objects.IsArray() && objects.Size() > 0 { + size += objects.Size() + for _, object := range objects.MustArray() { + prices += object.MustKey("price").MustNumeric() + } + } else if objects.IsObject() && objects.HasKey("price") { + size++ + prices += objects.MustKey("price").MustNumeric() + } + } + + if size > 0 { + fmt.Println("AVG price:", prices/float64(size)) + } else { + fmt.Println("AVG price:", 0) + } +} +``` + +## JSONPath: + +[Playground](https://play.golang.org/p/7twZHOd6dbT) +```go +package main + +import ( + "fmt" + "github.com/spyzhov/ajson" +) + +func main() { + data := []byte(`{"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}, "tools": null}}`) + + nodes, err := ajson.JSONPath(data, "$..price") + if err != nil { + panic(err) + } + + var prices float64 + size := len(nodes) + for _, node := range nodes { + prices += node.MustNumeric() + } + + if size > 0 { + fmt.Println("AVG price:", prices/float64(size)) + } else { + fmt.Println("AVG price:", 0) + } +} +``` + +## Eval + +[Playground](https://play.golang.org/p/lTXnlRU3sgR) +```go +package main + +import ( + "fmt" + "github.com/spyzhov/ajson" +) + +func main() { + json := []byte(`{"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}, "tools": null}}`) + root, err := ajson.Unmarshal(json) + if err != nil { + panic(err) + } + result, err := ajson.Eval(root, "avg($..price)") + if err != nil { + panic(err) + } + fmt.Println("AVG price:", result.MustNumeric()) +} +``` + +## Marshal + +[Playground](https://play.golang.org/p/i4gXXcA2VLU) +```go +package main + +import ( + "fmt" + "github.com/spyzhov/ajson" +) + +func main() { + json := []byte(`{"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}, "tools": null}}`) + root := ajson.Must(ajson.Unmarshal(json)) + result := ajson.Must(ajson.Eval(root, "avg($..price)")) + err := root.AppendObject("price(avg)", result) + if err != nil { + panic(err) + } + marshalled, err := ajson.Marshal(root) + if err != nil { + panic(err) + } + fmt.Printf("%s", marshalled) +} +``` + +# Benchmarks + +Current package is comparable with `encoding/json` package. + +Test data: +```json +{ "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 + } + } +} +``` +JSONPath: `$.store..price` + +``` +$ go test -bench=. -cpu=1 -benchmem +goos: linux +goarch: amd64 +pkg: github.com/spyzhov/ajson +BenchmarkUnmarshal_AJSON 138032 8762 ns/op 5344 B/op 95 allocs/op +BenchmarkUnmarshal_JSON 117423 10502 ns/op 968 B/op 31 allocs/op +BenchmarkJSONPath_all_prices 80908 14394 ns/op 7128 B/op 153 allocs/op +``` + +# License + +MIT licensed. See the [LICENSE](LICENSE) file for details. diff --git a/vendor/github.com/spyzhov/ajson/buffer.go b/vendor/github.com/spyzhov/ajson/buffer.go new file mode 100644 index 0000000..e332877 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/buffer.go @@ -0,0 +1,830 @@ +package ajson + +import ( + "io" + "strings" + + . "github.com/spyzhov/ajson/internal" +) + +type buffer struct { + data []byte + length int + index int + + last States + state States + class Classes +} + +const __ = -1 + +const ( + quotes byte = '"' + quote byte = '\'' + coma byte = ',' + colon byte = ':' + backslash byte = '\\' + skipS byte = ' ' + skipN byte = '\n' + skipR byte = '\r' + skipT byte = '\t' + bracketL byte = '[' + bracketR byte = ']' + bracesL byte = '{' + bracesR byte = '}' + parenthesesL byte = '(' + parenthesesR byte = ')' + dollar byte = '$' + at byte = '@' + dot byte = '.' + asterisk byte = '*' + plus byte = '+' + minus byte = '-' + //division byte = '/' + //exclamation byte = '!' + //caret byte = '^' + //signL byte = '<' + //signG byte = '>' + //signE byte = '=' + //ampersand byte = '&' + //pipe byte = '|' + question byte = '?' +) + +type ( + rpn []string + tokens []string +) + +var ( + _null = []byte("null") + _true = []byte("true") + _false = []byte("false") +) + +func newBuffer(body []byte) (b *buffer) { + b = &buffer{ + length: len(body), + data: body, + last: GO, + state: GO, + } + return +} + +func (b *buffer) current() (c byte, err error) { + if b.index < b.length { + return b.data[b.index], nil + } + return 0, io.EOF +} + +func (b *buffer) next() (c byte, err error) { + err = b.step() + if err != nil { + return 0, err + } + return b.data[b.index], nil +} + +func (b *buffer) slice(delta uint) ([]byte, error) { + if b.index+int(delta) > b.length { + return nil, io.EOF + } + return b.data[b.index : b.index+int(delta)], nil +} + +func (b *buffer) move(delta int) error { + if b.index+delta >= b.length { + return io.EOF + } + if b.index+delta >= 0 { + b.index += delta + } + return nil +} + +func (b *buffer) reset() { + b.last = GO +} + +func (b *buffer) first() (c byte, err error) { + for ; b.index < b.length; b.index++ { + c = b.data[b.index] + if !(c == skipS || c == skipR || c == skipN || c == skipT) { + return c, nil + } + } + return 0, io.EOF +} + +func (b *buffer) backslash() (result bool) { + for i := b.index - 1; i >= 0; i-- { + if b.data[i] == backslash { + result = !result + } else { + break + } + } + return +} + +func (b *buffer) skip(s byte) error { + for ; b.index < b.length; b.index++ { + if b.data[b.index] == s && !b.backslash() { + return nil + } + } + return io.EOF +} + +func (b *buffer) skipAny(s map[byte]bool) error { + for ; b.index < b.length; b.index++ { + if s[b.data[b.index]] && !b.backslash() { + return nil + } + } + return io.EOF +} + +// if token is true - skip error from StateTransitionTable, just stop on unknown state +func (b *buffer) numeric(token bool) error { + if token { + b.last = GO + } + for ; b.index < b.length; b.index++ { + b.class = b.getClasses(quotes) + if b.class == __ { + return b.errorSymbol() + } + b.state = StateTransitionTable[b.last][b.class] + if b.state == __ { + if token { + break + } + return b.errorSymbol() + } + if b.state < __ { + return nil + } + if b.state < MI || b.state > E3 { + return nil + } + b.last = b.state + } + if b.last != ZE && b.last != IN && b.last != FR && b.last != E3 { + return b.errorSymbol() + } + return nil +} + +func (b *buffer) getClasses(search byte) Classes { + if b.data[b.index] >= 128 { + return C_ETC + } + if search == quote { + return QuoteAsciiClasses[b.data[b.index]] + } + return AsciiClasses[b.data[b.index]] +} + +func (b *buffer) getState() States { + b.last = b.state + b.class = b.getClasses(quotes) + if b.class == __ { + return __ + } + b.state = StateTransitionTable[b.last][b.class] + return b.state +} + +func (b *buffer) string(search byte, token bool) error { + if token { + b.last = GO + } + for ; b.index < b.length; b.index++ { + b.class = b.getClasses(search) + + if b.class == __ { + return b.errorSymbol() + } + b.state = StateTransitionTable[b.last][b.class] + if b.state == __ { + return b.errorSymbol() + } + if b.state < __ { + return nil + } + b.last = b.state + } + return b.errorSymbol() +} + +func (b *buffer) null() error { + return b.word(_null) +} + +func (b *buffer) true() error { + return b.word(_true) +} + +func (b *buffer) false() error { + return b.word(_false) +} + +func (b *buffer) word(word []byte) error { + var c byte + max := len(word) + index := 0 + for ; b.index < b.length; b.index++ { + c = b.data[b.index] + // if c != word[index] && c != (word[index]-32) { + if c != word[index] { + return errorSymbol(b) + } + index++ + if index >= max { + break + } + } + if index != max { + return errorEOF(b) + } + return nil +} + +func (b *buffer) step() error { + if b.index+1 < b.length { + b.index++ + return nil + } + return io.EOF +} + +// reads until the end of the token e.g.: `@.length`, `@['foo'].bar[(@.length - 1)].baz` +func (b *buffer) token() (err error) { + var ( + c byte + stack = make([]byte, 0) + first = b.index + start int + find bool + ) +tokenLoop: + for ; b.index < b.length; b.index++ { + c = b.data[b.index] + switch { + case c == quotes: + fallthrough + case c == quote: + find = true + err = b.step() + if err != nil { + return b.errorEOF() + } + err = b.skip(c) + if err == io.EOF { + return b.errorEOF() + } + case c == bracketL: + find = true + stack = append(stack, c) + case c == bracketR: + find = true + if len(stack) == 0 { + if first == b.index { + return b.errorSymbol() + } + break tokenLoop + } + if stack[len(stack)-1] != bracketL { + return b.errorSymbol() + } + stack = stack[:len(stack)-1] + case c == parenthesesL: + find = true + stack = append(stack, c) + case c == parenthesesR: + find = true + if len(stack) == 0 { + if first == b.index { + return b.errorSymbol() + } + break tokenLoop + } + if stack[len(stack)-1] != parenthesesL { + return b.errorSymbol() + } + stack = stack[:len(stack)-1] + case c == dot || c == at || c == dollar || c == question || c == asterisk || (c >= 'A' && c <= 'z') || (c >= '0' && c <= '9'): // standard token name + find = true + continue + case len(stack) != 0: + find = true + continue + case c == minus || c == plus: + if !find { + find = true + start = b.index + err = b.numeric(true) + if err == nil || err == io.EOF { + b.index-- + continue + } + b.index = start + } + fallthrough + default: + break tokenLoop + } + } + if len(stack) != 0 { + return b.errorEOF() + } + if first == b.index { + return b.step() + } + if b.index >= b.length { + return io.EOF + } + return nil +} + +// Builder for `Reverse Polish notation` +func (b *buffer) rpn() (result rpn, err error) { + var ( + c byte + start int + temp string + current string + found bool + variable bool + stack = make([]string, 0) + ) + for { + b.reset() + c, err = b.first() + if err != nil { + break + } + switch true { + case priorityChar[c]: // operations + if variable { + variable = false + current = b.operation() + + if current == "" { + return nil, b.errorSymbol() + } + + for len(stack) > 0 { + temp = stack[len(stack)-1] + found = false + if temp[0] >= 'A' && temp[0] <= 'z' { // function + found = true + } else if priority[temp] != 0 { // operation + if priority[temp] > priority[current] { + found = true + } else if priority[temp] == priority[current] && !rightOp[temp] { + found = true + } + } + + if found { + stack = stack[:len(stack)-1] + result = append(result, temp) + } else { + break + } + } + stack = append(stack, current) + break + } + if c != minus && c != plus { + return nil, b.errorSymbol() + } + fallthrough // for numbers like `-1e6` + case (c >= '0' && c <= '9') || c == '.': // numbers + variable = true + start = b.index + err = b.numeric(true) + if err != nil { + return nil, err + } + current = string(b.data[start:b.index]) + result = append(result, current) + b.index-- + case c == quotes: // string + fallthrough + case c == quote: // string + variable = true + start = b.index + err = b.string(c, true) + if err != nil { + return nil, b.errorEOF() + } + current = string(b.data[start : b.index+1]) + result = append(result, current) + case c == dollar || c == at: // variable : like @.length , $.expensive, etc. + variable = true + start = b.index + err = b.token() + if err != nil { + if err != io.EOF { + return nil, err + } + } + current = string(b.data[start:b.index]) + result = append(result, current) + if err != nil { + //nolint:ineffassign + err = nil + } else { + b.index-- + } + case c == parenthesesL: // ( + variable = false + current = string(c) + stack = append(stack, current) + case c == parenthesesR: // ) + variable = true + found = false + for len(stack) > 0 { + temp = stack[len(stack)-1] + stack = stack[:len(stack)-1] + if temp == "(" { + found = true + break + } + result = append(result, temp) + } + if !found { // have no parenthesesL + return nil, errorRequest("formula has no left parentheses") + } + default: // prefix functions or etc. + start = b.index + variable = true + for ; b.index < b.length; b.index++ { + c = b.data[b.index] + if c == parenthesesL { // function detection, example: sin(...), round(...), etc. + variable = false + break + } + if c < 'A' || c > 'z' { + if !(c >= '0' && c <= '9') && c != '_' { // constants detection, example: true, false, null, PI, e, etc. + break + } + } + } + current = strings.ToLower(string(b.data[start:b.index])) + b.index-- + if !variable { + if _, found = functions[current]; !found { + return nil, errorRequest("wrong formula, '%s' is not a function", current) + } + stack = append(stack, current) + } else { + if _, found = constants[current]; !found { + return nil, errorRequest("wrong formula, '%s' is not a constant", current) + } + result = append(result, current) + } + } + err = b.step() + if err != nil { + break + } + } + if err == io.EOF { + err = nil // only io.EOF can be here + } + + for len(stack) > 0 { + temp = stack[len(stack)-1] + _, ok := functions[temp] + if priority[temp] == 0 && !ok { // operations only + return nil, errorRequest("wrong formula, '%s' is not an operation or function", temp) + } + result = append(result, temp) + stack = stack[:len(stack)-1] + } + + if len(result) == 0 { + return nil, b.errorEOF() + } + + return +} + +func (b *buffer) tokenize() (result tokens, err error) { + var ( + c byte + start int + current string + variable bool + ) + for { + b.reset() + c, err = b.first() + if err != nil { + break + } + switch true { + case priorityChar[c]: // operations + if variable || (c != minus && c != plus) { + variable = false + current = b.operation() + + if current == "" { + return nil, b.errorSymbol() + } + + result = append(result, current) + break + } + fallthrough // for numbers like `-1e6` + case (c >= '0' && c <= '9') || c == dot: // numbers + variable = true + start = b.index + err = b.numeric(true) + if err != nil && err != io.EOF { + if c == dot { + //nolint:ineffassign + err = nil + result = append(result, ".") + b.index = start + break + } + return nil, err + } + current = string(b.data[start:b.index]) + result = append(result, current) + b.index-- + case c == quotes: // string + fallthrough + case c == quote: // string + variable = true + start = b.index + err = b.string(c, true) + if err != nil { + return nil, b.errorEOF() + } + current = string(b.data[start : b.index+1]) + result = append(result, current) + case c == dollar || c == at: // variable : like @.length , $.expensive, etc. + variable = true + start = b.index + err = b.token() + if err != nil { + if err != io.EOF { + return nil, err + } + } + current = string(b.data[start:b.index]) + result = append(result, current) + if err != nil { + //nolint:ineffassign + err = nil + } else { + b.index-- + } + case c == parenthesesL: // ( + variable = false + current = string(c) + result = append(result, current) + case c == parenthesesR: // ) + variable = true + current = string(c) + result = append(result, current) + default: // prefix functions or etc. + start = b.index + variable = true + for ; b.index < b.length; b.index++ { + c = b.data[b.index] + if c == parenthesesL { // function detection, example: sin(...), round(...), etc. + variable = false + break + } + if c < 'A' || c > 'z' { + if !(c >= '0' && c <= '9') && c != '_' { // constants detection, example: true, false, null, PI, e, etc. + break + } + } + } + if start == b.index { + err = b.step() + if err != nil { + //nolint:ineffassign + err = nil + current = strings.ToLower(string(b.data[start : b.index+1])) + } else { + current = strings.ToLower(string(b.data[start:b.index])) + b.index-- + } + } else { + current = strings.ToLower(string(b.data[start:b.index])) + b.index-- + } + result = append(result, current) + } + err = b.step() + if err != nil { + break + } + } + + if err == io.EOF { + err = nil + } + + return +} + +func (b *buffer) operation() string { + current := "" + + // Read the complete operation into the variable `current`: `+`, `!=`, `<=>` + // fixme: add additional order for comparison + + for _, operation := range comparisonOperationsOrder() { + if bytes, ok := b.slice(uint(len(operation))); ok == nil { + if string(bytes) == operation { + current = operation + _ = b.move(len(operation) - 1) // error can't occupy here because of b.slice result + break + } + } + } + return current +} + +func (b *buffer) errorEOF() error { + return errorEOF(b) +} + +func (b *buffer) errorSymbol() error { + return errorSymbol(b) +} + +func _floats(left, right *Node) (lnum, rnum float64, err error) { + lnum, err = left.GetNumeric() + if err != nil { + return + } + rnum, err = right.GetNumeric() + return +} + +func _ints(left, right *Node) (lnum, rnum int, err error) { + lnum, err = left.getInteger() + if err != nil { + return + } + rnum, err = right.getInteger() + return +} + +func _bools(left, right *Node) (lnum, rnum bool, err error) { + lnum, err = left.GetBool() + if err != nil { + return + } + rnum, err = right.GetBool() + return +} + +func _strings(left, right *Node) (lnum, rnum string, err error) { + lnum, err = left.GetString() + if err != nil { + return + } + rnum, err = right.GetString() + return +} + +func _arrays(left, right *Node) (lnum, rnum []*Node, err error) { + lnum, err = left.GetArray() + if err != nil { + return + } + rnum, err = right.GetArray() + return +} + +func _objects(left, right *Node) (lnum, rnum map[string]*Node, err error) { + lnum, err = left.GetObject() + if err != nil { + return + } + rnum, err = right.GetObject() + return +} + +func boolean(node *Node) (bool, error) { + switch node.Type() { + case Bool: + return node.GetBool() + case Numeric: + res, err := node.GetNumeric() + return res != 0, err + case String: + res, err := node.GetString() + return res != "", err + case Null: + return false, nil + case Array: + fallthrough + case Object: + return !node.Empty(), nil + } + return false, nil +} + +func tokenize(cmd string) (result tokens, err error) { + buf := newBuffer([]byte(cmd)) + return buf.tokenize() +} + +func (t tokens) exists(find string) bool { + for _, s := range t { + if s == find { + return true + } + } + return false +} + +func (t tokens) count(find string) int { + i := 0 + for _, s := range t { + if s == find { + i++ + } + } + return i +} + +func (t tokens) slice(find string) []string { + n := len(t) + result := make([]string, 0, t.count(find)) + from := 0 + for i := 0; i < n; i++ { + if t[i] == find { + result = append(result, strings.Join(t[from:i], "")) + from = i + 1 + } + } + result = append(result, strings.Join(t[from:n], "")) + return result +} + +func str(key string) (string, bool) { + bString := []byte(key) + from := len(bString) + if from > 1 && (bString[0] == quotes && bString[from-1] == quotes) { + return unquote(bString, quotes) + } + if from > 1 && (bString[0] == quote && bString[from-1] == quote) { + return unquote(bString, quote) + } + return key, true + // todo quote string and unquote it: + // { + // bString = append([]byte{quotes}, bString...) + // bString = append(bString, quotes) + // } + // return unquote(bString, quotes) +} + +func numeric2float64(value interface{}) (result float64, err error) { + switch typed := value.(type) { + case float64: + result = typed + case float32: + result = float64(typed) + case int: + result = float64(typed) + case int8: + result = float64(typed) + case int16: + result = float64(typed) + case int32: + result = float64(typed) + case int64: + result = float64(typed) + case uint: + result = float64(typed) + case uint8: + result = float64(typed) + case uint16: + result = float64(typed) + case uint32: + result = float64(typed) + case uint64: + result = float64(typed) + default: + err = unsupportedType(value) + } + return +} diff --git a/vendor/github.com/spyzhov/ajson/decode.go b/vendor/github.com/spyzhov/ajson/decode.go new file mode 100644 index 0000000..8d9a994 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/decode.go @@ -0,0 +1,231 @@ +package ajson + +import ( + . "github.com/spyzhov/ajson/internal" +) + +// List of action codes. +// Copy from `internal/state.go:144` +const ( + cl States = -2 /* colon */ + cm States = -3 /* comma */ + //qt States = -4 /* quote */ + bo States = -5 /* bracket open */ + co States = -6 /* curly br. open */ + bc States = -7 /* bracket close */ + cc States = -8 /* curly br. close */ + ec States = -9 /* curly br. empty */ +) + +// Unmarshal parses the JSON-encoded data and return the root node of struct. +// +// Doesn't calculate values, just type of stored value. It will store link to the data, on all life long. +func Unmarshal(data []byte) (root *Node, err error) { + buf := newBuffer(data) + var ( + state States + key *string + current *Node + useKey = func() **string { + tmp := cptrs(key) + key = nil + return &tmp + } + ) + + _, err = buf.first() + if err != nil { + return nil, buf.errorEOF() + } + + for { + state = buf.getState() + if state == __ { + return nil, buf.errorSymbol() + } + + if state >= GO { + // region Change State + switch buf.state { + case ST: + if current != nil && current.IsObject() && key == nil { + // Detected: Key + key, err = getString(buf) + buf.state = CO + } else { + // Detected: String + current, err = newNode(current, buf, String, useKey()) + if err != nil { + break + } + err = buf.string(quotes, false) + current.borders[1] = buf.index + 1 + buf.state = OK + if current.parent != nil { + current = current.parent + } + } + case MI, ZE, IN: + current, err = newNode(current, buf, Numeric, useKey()) + if err != nil { + break + } + err = buf.numeric(false) + current.borders[1] = buf.index + buf.index -= 1 + buf.state = OK + if current.parent != nil { + current = current.parent + } + case T1, F1: + current, err = newNode(current, buf, Bool, useKey()) + if err != nil { + break + } + if buf.state == T1 { + err = buf.true() + } else { + err = buf.false() + } + current.borders[1] = buf.index + 1 + buf.state = OK + if current.parent != nil { + current = current.parent + } + case N1: + current, err = newNode(current, buf, Null, useKey()) + if err != nil { + break + } + err = buf.null() + current.borders[1] = buf.index + 1 + buf.state = OK + if current.parent != nil { + current = current.parent + } + } + // endregion Change State + } else { + // region Action + switch state { + case ec: /* empty } */ + if key != nil { + err = buf.errorSymbol() + } + fallthrough + case cc: /* } */ + if current != nil && current.IsObject() && !current.ready() { + current.borders[1] = buf.index + 1 + if current.parent != nil { + current = current.parent + } + } else { + err = buf.errorSymbol() + } + buf.state = OK + case bc: /* ] */ + if current != nil && current.IsArray() && !current.ready() { + current.borders[1] = buf.index + 1 + if current.parent != nil { + current = current.parent + } + } else { + err = buf.errorSymbol() + } + buf.state = OK + case co: /* { */ + current, err = newNode(current, buf, Object, useKey()) + buf.state = OB + case bo: /* [ */ + current, err = newNode(current, buf, Array, useKey()) + buf.state = AR + case cm: /* , */ + if current == nil { + return nil, buf.errorSymbol() + } + if current.IsObject() { + buf.state = KE + } else if current.IsArray() { + buf.state = VA + } else { + err = buf.errorSymbol() + } + case cl: /* : */ + if current == nil || !current.IsObject() || key == nil { + err = buf.errorSymbol() + } else { + buf.state = VA + } + default: /* syntax error */ + err = buf.errorSymbol() + } + // endregion Action + } + if err != nil { + return + } + if buf.step() != nil { + break + } + if _, err = buf.first(); err != nil { + err = nil + break + } + } + + if current == nil || buf.state != OK { + err = buf.errorEOF() + } else { + root = current.root() + if !root.ready() { + err = buf.errorEOF() + root = nil + } + } + + return +} + +// UnmarshalSafe do the same thing as Unmarshal, but copy data to the local variable, to make it editable. +func UnmarshalSafe(data []byte) (root *Node, err error) { + var safe []byte + safe = append(safe, data...) + return Unmarshal(safe) +} + +// Must returns a Node if there was no error. Else - panic with error as the value. +func Must(root *Node, err error) *Node { + if err != nil { + panic(err) + } + return root +} + +func getString(b *buffer) (*string, error) { + start := b.index + err := b.string(quotes, false) + if err != nil { + return nil, err + } + value, ok := unquote(b.data[start:b.index+1], quotes) + if !ok { + return nil, errorSymbol(b) + } + return &value, nil +} + +func cptrs(cpy *string) *string { + if cpy == nil { + return nil + } + val := *cpy + return &val +} + +func cptri(cpy *int) *int { + if cpy == nil { + return nil + } + val := *cpy + return &val +} diff --git a/vendor/github.com/spyzhov/ajson/doc.go b/vendor/github.com/spyzhov/ajson/doc.go new file mode 100644 index 0000000..6c5d019 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/doc.go @@ -0,0 +1,163 @@ +// Package ajson implements decoding of JSON as defined in RFC 7159 without predefined mapping to a struct of golang, with support of JSONPath. +// +// All JSON structs reflects to a custom struct of Node, witch can be presented by it type and value. +// +// Method Unmarshal will scan all the byte slice to create a root node of JSON structure, with all it behaviors. +// +// Each Node has it's own type and calculated value, which will be calculated on demand. +// Calculated value saves in atomic.Value, so it's thread safe. +// +// Method JSONPath will returns slice of founded elements in current JSON data, by it's JSONPath. +// +// JSONPath selection described at http://goessner.net/articles/JsonPath/ +// +// JSONPath expressions always refer to a JSON structure in the same way as XPath expression are used in combination with an XML document. Since a JSON structure is usually anonymous and doesn't necessarily have a "root member object" JSONPath assumes the abstract name $ assigned to the outer level object. +// +// JSONPath expressions can use the dot–notation +// +// $.store.book[0].title +// +// or the bracket–notation +// +// $['store']['book'][0]['title'] +// +// for input pathes. Internal or output pathes will always be converted to the more general bracket–notation. +// +// JSONPath allows the wildcard symbol * for member names and array indices. It borrows the descendant operator '..' from E4X and the array slice syntax proposal [start:end:step] from ECMASCRIPT 4. +// +// Expressions of the underlying scripting language () can be used as an alternative to explicit names or indices as in +// +// $.store.book[(@.length-1)].title +// +// using the symbol '@' for the current object. Filter expressions are supported via the syntax ?() as in +// +// $.store.book[?(@.price < 10)].title +// +// Here is a complete overview and a side by side comparison of the JSONPath syntax elements with its XPath counterparts. +// +// $ the root object/element +// @ the current object/element +// . or [] child operator +// .. recursive descent. JSONPath borrows this syntax from E4X. +// * wildcard. All objects/elements regardless their names. +// [] subscript operator. XPath uses it to iterate over element collections and for predicates. In Javascript and JSON it is the native array operator. +// [,] Union operator in XPath results in a combination of node sets. JSONPath allows alternate names or array indices as a set. +// [start:end:step] array slice operator borrowed from ES4. +// ?() applies a filter (script) expression. +// () script expression, using the underlying script engine. +// +// +// JSONPath Script engine +// +// Predefined constant +// +// Package has several predefined constants. You are free to add new one with AddConstant +// +// e math.E float64 +// pi math.Pi float64 +// phi math.Phi float64 +// +// sqrt2 math.Sqrt2 float64 +// sqrte math.SqrtE float64 +// sqrtpi math.SqrtPi float64 +// sqrtphi math.SqrtPhi float64 +// +// ln2 math.Ln2 float64 +// log2e math.Log2E float64 +// ln10 math.Ln10 float64 +// log10e math.Log10E float64 +// +// true true bool +// false false bool +// null nil interface{} +// +// Supported operations +// +// Package has several predefined operators. You are free to add new one with AddOperator +// +// Operator precedence: https://golang.org/ref/spec#Operator_precedence +// +// Precedence Operator +// 6 ** +// 5 * / % << >> & &^ +// 4 + - | ^ +// 3 == != < <= > >= =~ +// 2 && +// 1 || +// +// Arithmetic operators: https://golang.org/ref/spec#Arithmetic_operators +// +// ** power integers, floats +// + sum integers, floats, strings +// - difference integers, floats +// * product integers, floats +// / quotient integers, floats +// % remainder integers +// +// & bitwise AND integers +// | bitwise OR integers +// ^ bitwise XOR integers +// &^ bit clear (AND NOT) integers +// +// << left shift integer << unsigned integer +// >> right shift integer >> unsigned integer +// +// == equals any +// != not equals any +// < less any +// <= less or equals any +// > larger any +// >= larger or equals any +// =~ equals regex string strings +// +// Supported functions +// +// Package has several predefined functions. You are free to add new one with AddFunction +// +// abs math.Abs integers, floats +// acos math.Acos integers, floats +// acosh math.Acosh integers, floats +// asin math.Asin integers, floats +// asinh math.Asinh integers, floats +// atan math.Atan integers, floats +// atanh math.Atanh integers, floats +// avg Average array of integers or floats +// b64decode b64 Decoding string +// b64encode b64 Encoding string +// b64encoden b64 Encoding (no padding) string +// cbrt math.Cbrt integers, floats +// ceil math.Ceil integers, floats +// cos math.Cos integers, floats +// cosh math.Cosh integers, floats +// erf math.Erf integers, floats +// erfc math.Erfc integers, floats +// erfcinv math.Erfcinv integers, floats +// erfinv math.Erfinv integers, floats +// exp math.Exp integers, floats +// exp2 math.Exp2 integers, floats +// expm1 math.Expm1 integers, floats +// factorial N! unsigned integer +// floor math.Floor integers, floats +// gamma math.Gamma integers, floats +// j0 math.J0 integers, floats +// j1 math.J1 integers, floats +// length len array +// log math.Log integers, floats +// log10 math.Log10 integers, floats +// log1p math.Log1p integers, floats +// log2 math.Log2 integers, floats +// logb math.Logb integers, floats +// not not any +// pow10 math.Pow10 integer +// round math.Round integers, floats +// roundtoeven math.RoundToEven integers, floats +// sin math.Sin integers, floats +// sinh math.Sinh integers, floats +// sqrt math.Sqrt integers, floats +// tan math.Tan integers, floats +// tanh math.Tanh integers, floats +// trunc math.Trunc integers, floats +// y0 math.Y0 integers, floats +// y1 math.Y1 integers, floats +// +package ajson diff --git a/vendor/github.com/spyzhov/ajson/encode.go b/vendor/github.com/spyzhov/ajson/encode.go new file mode 100644 index 0000000..9dfd6f2 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/encode.go @@ -0,0 +1,90 @@ +package ajson + +import ( + "strconv" +) + +// Marshal returns slice of bytes, marshaled from current value +func Marshal(node *Node) (result []byte, err error) { + result = make([]byte, 0) + var ( + sValue string + bValue bool + nValue float64 + oValue []byte + ) + + if node == nil { + return nil, errorUnparsed() + } else if node.dirty { + switch node._type { + case Null: + result = append(result, _null...) + case Numeric: + nValue, err = node.GetNumeric() + if err != nil { + return nil, err + } + result = append(result, strconv.FormatFloat(nValue, 'g', -1, 64)...) + case String: + sValue, err = node.GetString() + if err != nil { + return nil, err + } + result = append(result, quotes) + result = append(result, quoteString(sValue, true)...) + result = append(result, quotes) + case Bool: + bValue, err = node.GetBool() + if err != nil { + return nil, err + } else if bValue { + result = append(result, _true...) + } else { + result = append(result, _false...) + } + case Array: + result = append(result, bracketL) + for i := 0; i < len(node.children); i++ { + if i != 0 { + result = append(result, coma) + } + child, ok := node.children[strconv.Itoa(i)] + if !ok { + return nil, errorRequest("wrong length of array") + } + oValue, err = Marshal(child) + if err != nil { + return nil, err + } + result = append(result, oValue...) + } + result = append(result, bracketR) + case Object: + result = append(result, bracesL) + bValue = false + for key, child := range node.children { + if bValue { + result = append(result, coma) + } else { + bValue = true + } + result = append(result, quotes) + result = append(result, quoteString(key, true)...) + result = append(result, quotes, colon) + oValue, err = Marshal(child) + if err != nil { + return nil, err + } + result = append(result, oValue...) + } + result = append(result, bracesR) + } + } else if node.ready() { + result = append(result, node.Source()...) + } else { + return nil, errorUnparsed() + } + + return +} diff --git a/vendor/github.com/spyzhov/ajson/errors.go b/vendor/github.com/spyzhov/ajson/errors.go new file mode 100644 index 0000000..523d0ae --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/errors.go @@ -0,0 +1,102 @@ +package ajson + +import "fmt" + +// Error is common struct to provide internal errors +type Error struct { + Type ErrorType + Index int + Char byte + Message string + Value interface{} +} + +// ErrorType is container for reflection type of error +type ErrorType int + +const ( + // WrongSymbol means that system found symbol than not allowed to be + WrongSymbol ErrorType = iota + // UnexpectedEOF means that data ended, leaving the node undone + UnexpectedEOF + // WrongType means that wrong type requested + WrongType + // WrongRequest means that wrong range requested + WrongRequest + // Unparsed means that json structure wasn't parsed yet + Unparsed + // UnsupportedType means that wrong type was given + UnsupportedType +) + +func errorSymbol(b *buffer) error { + symbol, err := b.current() + if err != nil { + symbol = 0 + } + return Error{ + Type: WrongSymbol, + Index: b.index, + Char: symbol, + } +} + +func errorAt(index int, symbol byte) error { + return Error{ + Type: WrongSymbol, + Index: index, + Char: symbol, + } +} + +func errorEOF(b *buffer) error { + return Error{ + Type: UnexpectedEOF, + Index: b.index, + } +} + +func errorType() error { + return Error{ + Type: WrongType, + } +} + +func unsupportedType(value interface{}) error { + return Error{ + Type: UnsupportedType, + Value: value, + } +} + +func errorUnparsed() error { + return Error{ + Type: Unparsed, + } +} + +func errorRequest(format string, args ...interface{}) error { + return Error{ + Type: WrongRequest, + Message: fmt.Sprintf(format, args...), + } +} + +// Error interface implementation +func (err Error) Error() string { + switch err.Type { + case WrongSymbol: + return fmt.Sprintf("wrong symbol '%s' at %d", []byte{err.Char}, err.Index) + case UnexpectedEOF: + return "unexpected end of file" + case WrongType: + return "wrong type of Node" + case UnsupportedType: + return fmt.Sprintf("unsupported type was given: '%T'", err.Value) + case Unparsed: + return "not parsed yet" + case WrongRequest: + return fmt.Sprintf("wrong request: %s", err.Message) + } + return fmt.Sprintf("unknown error: '%s' at %d", []byte{err.Char}, err.Index) +} diff --git a/vendor/github.com/spyzhov/ajson/internal/state.go b/vendor/github.com/spyzhov/ajson/internal/state.go new file mode 100644 index 0000000..16bc2fc --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/internal/state.go @@ -0,0 +1,196 @@ +/* +Copy from https://github.com/freddierice/php_source/blob/467ed5d6edff72219afd3e644516f131118ef48e/ext/json/JSON_parser.c +Base code: Copyright (c) 2005 JSON.org +*/ +package internal + +type ( + States int8 + Classes int8 +) + +const __ = -1 + +// enum classes +const ( + C_SPACE Classes = iota /* space */ + C_WHITE /* other whitespace */ + C_LCURB /* { */ + C_RCURB /* } */ + C_LSQRB /* [ */ + C_RSQRB /* ] */ + C_COLON /* : */ + C_COMMA /* , */ + C_QUOTE /* " */ + C_BACKS /* \ */ + C_SLASH /* / */ + C_PLUS /* + */ + C_MINUS /* - */ + C_POINT /* . */ + C_ZERO /* 0 */ + C_DIGIT /* 123456789 */ + C_LOW_A /* a */ + C_LOW_B /* b */ + C_LOW_C /* c */ + C_LOW_D /* d */ + C_LOW_E /* e */ + C_LOW_F /* f */ + C_LOW_L /* l */ + C_LOW_N /* n */ + C_LOW_R /* r */ + C_LOW_S /* s */ + C_LOW_T /* t */ + C_LOW_U /* u */ + C_ABCDF /* ABCDF */ + C_E /* E */ + C_ETC /* everything else */ +) + +// AsciiClasses array maps the 128 ASCII characters into character classes. +var AsciiClasses = [128]Classes{ + /* + This array maps the 128 ASCII characters into character classes. + The remaining Unicode characters should be mapped to C_ETC. + Non-whitespace control characters are errors. + */ + __, __, __, __, __, __, __, __, + __, C_WHITE, C_WHITE, __, __, C_WHITE, __, __, + __, __, __, __, __, __, __, __, + __, __, __, __, __, __, __, __, + + C_SPACE, C_ETC, C_QUOTE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, + C_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH, + C_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, + C_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, + + C_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC, + C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, + C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, + C_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC, + + C_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC, + C_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC, + C_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC, + C_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC, +} + +// QuoteAsciiClasses is a HACK for single quote from AsciiClasses +var QuoteAsciiClasses = [128]Classes{ + /* + This array maps the 128 ASCII characters into character classes. + The remaining Unicode characters should be mapped to C_ETC. + Non-whitespace control characters are errors. + */ + __, __, __, __, __, __, __, __, + __, C_WHITE, C_WHITE, __, __, C_WHITE, __, __, + __, __, __, __, __, __, __, __, + __, __, __, __, __, __, __, __, + + C_SPACE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_QUOTE, + C_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH, + C_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, + C_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, + + C_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC, + C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, + C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, + C_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC, + + C_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC, + C_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC, + C_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC, + C_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC, +} + +/* +The state codes. +*/ +const ( + GO States = iota /* start */ + OK /* ok */ + OB /* object */ + KE /* key */ + CO /* colon */ + VA /* value */ + AR /* array */ + ST /* string */ + ES /* escape */ + U1 /* u1 */ + U2 /* u2 */ + U3 /* u3 */ + U4 /* u4 */ + MI /* minus */ + ZE /* zero */ + IN /* integer */ + DT /* dot */ + FR /* fraction */ + E1 /* e */ + E2 /* ex */ + E3 /* exp */ + T1 /* tr */ + T2 /* tru */ + T3 /* true */ + F1 /* fa */ + F2 /* fal */ + F3 /* fals */ + F4 /* false */ + N1 /* nu */ + N2 /* nul */ + N3 /* null */ +) + +// List of action codes +const ( + cl States = -2 /* colon */ + cm States = -3 /* comma */ + qt States = -4 /* quote */ + bo States = -5 /* bracket open */ + co States = -6 /* curly br. open */ + bc States = -7 /* bracket close */ + cc States = -8 /* curly br. close */ + ec States = -9 /* curly br. empty */ +) + +// StateTransitionTable is the state transition table takes the current state and the current symbol, and returns either +// a new state or an action. An action is represented as a negative number. A JSON text is accepted if at the end of the +// text the state is OK and if the mode is DONE. +var StateTransitionTable = [31][31]States{ + /* + The state transition table takes the current state and the current symbol, + and returns either a new state or an action. An action is represented as a + negative number. A JSON text is accepted if at the end of the text the + state is OK and if the mode is DONE. + white 1-9 ABCDF etc + space | { } [ ] : , " \ / + - . 0 | a b c d e f l n r s t u | E |*/ + /*start GO*/ {GO, GO, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __}, + /*ok OK*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*object OB*/ {OB, OB, __, ec, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*key KE*/ {KE, KE, __, __, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*colon CO*/ {CO, CO, __, __, __, __, cl, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*value VA*/ {VA, VA, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __}, + /*array AR*/ {AR, AR, co, __, bo, bc, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __}, + /*string ST*/ {ST, __, ST, ST, ST, ST, ST, ST, qt, ES, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST}, + /*escape ES*/ {__, __, __, __, __, __, __, __, ST, ST, ST, __, __, __, __, __, __, ST, __, __, __, ST, __, ST, ST, __, ST, U1, __, __, __}, + /*u1 U1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U2, U2, U2, U2, U2, U2, U2, U2, __, __, __, __, __, __, U2, U2, __}, + /*u2 U2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U3, U3, U3, U3, U3, U3, U3, U3, __, __, __, __, __, __, U3, U3, __}, + /*u3 U3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U4, U4, U4, U4, U4, U4, U4, U4, __, __, __, __, __, __, U4, U4, __}, + /*u4 U4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ST, ST, ST, ST, ST, ST, ST, ST, __, __, __, __, __, __, ST, ST, __}, + /*minus MI*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ZE, IN, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*zero ZE*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, __, __, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __}, + /*int IN*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, IN, IN, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __}, + /*dot DT*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, FR, FR, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*frac FR*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, FR, FR, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __}, + /*e E1*/ {__, __, __, __, __, __, __, __, __, __, __, E2, E2, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*ex E2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*exp E3*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*tr T1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T2, __, __, __, __, __, __}, + /*tru T2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T3, __, __, __}, + /*true T3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __}, + /*fa F1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F2, __, __, __, __, __, __, __, __, __, __, __, __, __, __}, + /*fal F2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F3, __, __, __, __, __, __, __, __}, + /*fals F3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F4, __, __, __, __, __}, + /*false F4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __}, + /*nu N1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N2, __, __, __}, + /*nul N2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N3, __, __, __, __, __, __, __, __}, + /*null N3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __}, +} diff --git a/vendor/github.com/spyzhov/ajson/jsonpath.go b/vendor/github.com/spyzhov/ajson/jsonpath.go new file mode 100644 index 0000000..055d8a4 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/jsonpath.go @@ -0,0 +1,710 @@ +package ajson + +import ( + "io" + "math" + "strconv" + "strings" +) + +// JSONPath returns slice of founded elements in current JSON data, by it's JSONPath. +// +// JSONPath described at http://goessner.net/articles/JsonPath/ +// +// JSONPath expressions always refer to a JSON structure in the same way as XPath expression are used in combination with an XML document. Since a JSON structure is usually anonymous and doesn't necessarily have a "root member object" JSONPath assumes the abstract name $ assigned to the outer level object. +// +// JSONPath expressions can use the dot–notation +// +// $.store.book[0].title +// +// or the bracket–notation +// +// $['store']['book'][0]['title'] +// +// for input pathes. Internal or output pathes will always be converted to the more general bracket–notation. +// +// JSONPath allows the wildcard symbol * for member names and array indices. It borrows the descendant operator '..' from E4X and the array slice syntax proposal [start:end:step] from ECMASCRIPT 4. +// +// Expressions of the underlying scripting language () can be used as an alternative to explicit names or indices as in +// +// $.store.book[(@.length-1)].title +// +// using the symbol '@' for the current object. Filter expressions are supported via the syntax ?() as in +// +// $.store.book[?(@.price < 10)].title +// +// Here is a complete overview and a side by side comparison of the JSONPath syntax elements with its XPath counterparts. +// +// $ the root object/element +// @ the current object/element +// . or [] child operator +// .. recursive descent. JSONPath borrows this syntax from E4X. +// * wildcard. All objects/elements regardless their names. +// [] subscript operator. XPath uses it to iterate over element collections and for predicates. In Javascript and JSON it is the native array operator. +// [,] Union operator in XPath results in a combination of node sets. JSONPath allows alternate names or array indices as a set. +// [start:end:step] array slice operator borrowed from ES4. +// ?() applies a filter (script) expression. +// () script expression, using the underlying script engine. +// +// # JSONPath Script engine +// +// # Predefined constant +// +// Package has several predefined constants. You are free to add new one with AddConstant +// +// e math.E float64 +// pi math.Pi float64 +// phi math.Phi float64 +// +// sqrt2 math.Sqrt2 float64 +// sqrte math.SqrtE float64 +// sqrtpi math.SqrtPi float64 +// sqrtphi math.SqrtPhi float64 +// +// ln2 math.Ln2 float64 +// log2e math.Log2E float64 +// ln10 math.Ln10 float64 +// log10e math.Log10E float64 +// +// true true bool +// false false bool +// null nil interface{} +// +// # Supported operations +// +// Package has several predefined operators. You are free to add new one with AddOperator +// +// Operator precedence: https://golang.org/ref/spec#Operator_precedence +// +// Precedence Operator +// 6 ** +// 5 * / % << >> & &^ +// 4 + - | ^ +// 3 == != < <= > >= =~ +// 2 && +// 1 || +// +// Arithmetic operators: https://golang.org/ref/spec#Arithmetic_operators +// +// ** power integers, floats +// + sum integers, floats, strings +// - difference integers, floats +// * product integers, floats +// / quotient integers, floats +// % remainder integers +// +// & bitwise AND integers +// | bitwise OR integers +// ^ bitwise XOR integers +// &^ bit clear (AND NOT) integers +// +// << left shift integer << unsigned integer +// >> right shift integer >> unsigned integer +// +// == equals any +// != not equals any +// < less any +// <= less or equals any +// > larger any +// >= larger or equals any +// =~ equals regex string strings +// +// # Supported functions +// +// Package has several predefined functions. You are free to add new one with AddFunction +// +// abs math.Abs integers, floats +// acos math.Acos integers, floats +// acosh math.Acosh integers, floats +// asin math.Asin integers, floats +// asinh math.Asinh integers, floats +// atan math.Atan integers, floats +// atanh math.Atanh integers, floats +// avg Average array of integers or floats +// cbrt math.Cbrt integers, floats +// ceil math.Ceil integers, floats +// cos math.Cos integers, floats +// cosh math.Cosh integers, floats +// erf math.Erf integers, floats +// erfc math.Erfc integers, floats +// erfcinv math.Erfcinv integers, floats +// erfinv math.Erfinv integers, floats +// exp math.Exp integers, floats +// exp2 math.Exp2 integers, floats +// expm1 math.Expm1 integers, floats +// factorial N! unsigned integer +// floor math.Floor integers, floats +// gamma math.Gamma integers, floats +// j0 math.J0 integers, floats +// j1 math.J1 integers, floats +// length len array +// log math.Log integers, floats +// log10 math.Log10 integers, floats +// log1p math.Log1p integers, floats +// log2 math.Log2 integers, floats +// logb math.Logb integers, floats +// not not any +// pow10 math.Pow10 integer +// round math.Round integers, floats +// roundtoeven math.RoundToEven integers, floats +// sin math.Sin integers, floats +// sinh math.Sinh integers, floats +// sqrt math.Sqrt integers, floats +// tan math.Tan integers, floats +// tanh math.Tanh integers, floats +// trunc math.Trunc integers, floats +// y0 math.Y0 integers, floats +// y1 math.Y1 integers, floats +func JSONPath(data []byte, path string) (result []*Node, err error) { + commands, err := ParseJSONPath(path) + if err != nil { + return nil, err + } + node, err := Unmarshal(data) + if err != nil { + return nil, err + } + return ApplyJSONPath(node, commands) +} + +// Paths returns calculated paths of underlying nodes +func Paths(array []*Node) []string { + result := make([]string, 0, len(array)) + for _, element := range array { + result = append(result, element.Path()) + } + return result +} + +func recursiveChildren(node *Node) (result []*Node) { + if node.isContainer() { + for _, element := range node.Inheritors() { + if element.isContainer() { + result = append(result, element) + } + } + } + temp := make([]*Node, 0, len(result)) + temp = append(temp, result...) + for _, element := range result { + temp = append(temp, recursiveChildren(element)...) + } + return temp +} + +// ParseJSONPath will parse current path and return all commands tobe run. +// Example: +// +// result, _ := ParseJSONPath("$.store.book[?(@.price < 10)].title") +// result == []string{"$", "store", "book", "?(@.price < 10)", "title"} +func ParseJSONPath(path string) (result []string, err error) { + buf := newBuffer([]byte(path)) + result = make([]string, 0) + const ( + fQuote = 1 << 0 + fQuotes = 1 << 1 + ) + var ( + c byte + start, stop int + childEnd = map[byte]bool{dot: true, bracketL: true} + flag int + brackets int + ) + for { + c, err = buf.current() + if err != nil { + break + } + parseSwitch: + switch true { + case c == dollar || c == at: + result = append(result, string(c)) + case c == dot: + start = buf.index + c, err = buf.next() + if err == io.EOF { + //nolint:ineffassign + err = nil + break + } + if err != nil { + break + } + if c == dot { + result = append(result, "..") + buf.index-- + break + } + err = buf.skipAny(childEnd) + stop = buf.index + if err == io.EOF { + err = nil + stop = buf.length + } else { + buf.index-- + } + if err != nil { + break + } + if start+1 < stop { + result = append(result, string(buf.data[start+1:stop])) + } + case c == bracketL: + _, err = buf.next() + if err != nil { + return nil, buf.errorEOF() + } + brackets = 1 + start = buf.index + for ; buf.index < buf.length; buf.index++ { + c = buf.data[buf.index] + switch c { + case quote: + if flag&fQuotes == 0 { + if flag&fQuote == 0 { + flag |= fQuote + } else if !buf.backslash() { + flag ^= fQuote + } + } + case quotes: + if flag&fQuote == 0 { + if flag&fQuotes == 0 { + flag |= fQuotes + } else if !buf.backslash() { + flag ^= fQuotes + } + } + case bracketL: + if flag == 0 && !buf.backslash() { + brackets++ + } + case bracketR: + if flag == 0 && !buf.backslash() { + brackets-- + } + if brackets == 0 { + result = append(result, string(buf.data[start:buf.index])) + break parseSwitch + } + } + } + return nil, buf.errorEOF() + default: + return nil, buf.errorSymbol() + } + err = buf.step() + if err != nil { + if err == io.EOF { + err = nil + } + break + } + } + return +} + +// ApplyJSONPath function applies commands sequence parse from JSONPath. +// Example: +// +// commands := []string{"$", "store", "book", "?(@.price < 10)", "title"} +// result, _ := ApplyJSONPath(node, commands) +func ApplyJSONPath(node *Node, commands []string) (result []*Node, err error) { + if node == nil { + return nil, nil + } + result = make([]*Node, 0) + var ( + temporary []*Node + keys []string + ikeys [3]int + fkeys [3]float64 + num int + key string + ok bool + value, temp *Node + float float64 + tokens tokens + expr rpn + ) + for i, cmd := range commands { + tokens, err = newBuffer([]byte(cmd)).tokenize() + if err != nil { + return + } + switch { + case cmd == "$": // root element + if i == 0 { + result = append(result, node.root()) + } + case cmd == "@": // current element + if i == 0 { + result = append(result, node) + } + case cmd == "..": // recursive descent + temporary = make([]*Node, 0) + for _, element := range result { + temporary = append(temporary, recursiveChildren(element)...) + } + result = append(result, temporary...) + case cmd == "*": // wildcard + temporary = make([]*Node, 0) + for _, element := range result { + temporary = append(temporary, element.Inheritors()...) + } + result = temporary + case tokens.exists(":"): // array slice operator + if tokens.count(":") > 3 { + return nil, errorRequest("slice must contains no more than 2 colons, got '%s'", cmd) + } + keys = tokens.slice(":") + + temporary = make([]*Node, 0) + for _, element := range result { + if element.IsArray() && element.Size() > 0 { + if fkeys[0], err = getNumberIndex(element, keys[0], math.NaN()); err != nil { + return nil, errorRequest("wrong request: %s", cmd) + } + if fkeys[1], err = getNumberIndex(element, keys[1], math.NaN()); err != nil { + return nil, errorRequest("wrong request: %s", cmd) + } + if len(keys) < 3 { + fkeys[2] = 1 + } else if fkeys[2], err = getNumberIndex(element, keys[2], 1); err != nil { + return nil, errorRequest("wrong request: %s", cmd) + } + + ikeys[2] = int(fkeys[2]) + if ikeys[2] == 0 { + return nil, errorRequest("wrong request: %s", cmd) + } + + if math.IsNaN(fkeys[0]) { + if ikeys[2] > 0 { + ikeys[0] = 0 + } else { + ikeys[0] = element.Size() - 1 + } + } else { + ikeys[0] = getPositiveIndex(int(fkeys[0]), element.Size()) + } + if math.IsNaN(fkeys[1]) { + if ikeys[2] > 0 { + ikeys[1] = element.Size() + } else { + ikeys[1] = -1 + } + } else { + ikeys[1] = getPositiveIndex(int(fkeys[1]), element.Size()) + } + + if ikeys[2] > 0 { + if ikeys[0] < 0 { + ikeys[0] = 0 + } + if ikeys[1] > element.Size() { + ikeys[1] = element.Size() + } + + for i := ikeys[0]; i < ikeys[1]; i += ikeys[2] { + value, ok := element.children[strconv.Itoa(i)] + if ok { + temporary = append(temporary, value) + } + } + } else if ikeys[2] < 0 { + if ikeys[0] > element.Size() { + ikeys[0] = element.Size() + } + if ikeys[1] < -1 { + ikeys[1] = -1 + } + + for i := ikeys[0]; i > ikeys[1]; i += ikeys[2] { + value, ok := element.children[strconv.Itoa(i)] + if ok { + temporary = append(temporary, value) + } + } + } + } + } + result = temporary + case strings.HasPrefix(cmd, "?(") && strings.HasSuffix(cmd, ")"): // applies a filter (script) expression + expr, err = newBuffer([]byte(cmd[2 : len(cmd)-1])).rpn() + if err != nil { + return nil, errorRequest("wrong request: %s", cmd) + } + temporary = make([]*Node, 0) + for _, element := range result { + if element.isContainer() { + for _, temp = range element.Inheritors() { + value, err = eval(temp, expr, cmd) + if err != nil { + return nil, errorRequest("wrong request: %s", cmd) + } + if value != nil { + ok, err = boolean(value) + if err != nil || !ok { + continue + } + temporary = append(temporary, temp) + } + } + } + } + result = temporary + case strings.HasPrefix(cmd, "(") && strings.HasSuffix(cmd, ")"): // script expression, using the underlying script engine + expr, err = newBuffer([]byte(cmd[1 : len(cmd)-1])).rpn() + if err != nil { + return nil, errorRequest("wrong request: %s", cmd) + } + temporary = make([]*Node, 0) + for _, element := range result { + if !element.isContainer() { + continue + } + temp, err = eval(element, expr, cmd) + if err != nil { + return nil, errorRequest("wrong request: %s", cmd) + } + if temp != nil { + value = nil + switch temp.Type() { + case String: + key, err = temp.GetString() + if err != nil { + return nil, errorRequest("wrong type convert: %s", err.Error()) + } + value = element.children[key] + case Numeric: + num, err = temp.getInteger() + if err == nil { // INTEGER + if num < 0 { + key = strconv.Itoa(element.Size() - num) + } else { + key = strconv.Itoa(num) + } + } else { + float, err = temp.GetNumeric() + if err != nil { + return nil, errorRequest("wrong type convert: %s", err.Error()) + } + key = strconv.FormatFloat(float, 'g', -1, 64) + } + value = element.children[key] + case Bool: + ok, err = temp.GetBool() + if err != nil { + return nil, errorRequest("wrong type convert: %s", err.Error()) + } + if ok { + temporary = append(temporary, element.Inheritors()...) + } + continue + // case Array: // get all keys from element via array values + } + if value != nil { + temporary = append(temporary, value) + } + } + } + result = temporary + default: // try to get by key & Union + if tokens.exists(",") { + keys = tokens.slice(",") + if len(keys) == 0 { + return nil, errorRequest("wrong request: %s", cmd) + } + } else { + keys = []string{cmd} + } + + temporary = make([]*Node, 0) + for _, key = range keys { // fixme + for _, element := range result { + if element.IsArray() { + if key == "length" || key == "'length'" || key == "\"length\"" { + value, err = functions["length"](element) + if err != nil { + return + } + ok = true + } else if strings.HasPrefix(key, "(") && strings.HasSuffix(key, ")") { + fkeys[0], err = getNumberIndex(element, key, math.NaN()) + if err != nil { + return nil, err + } + if math.IsNaN(fkeys[0]) { + return nil, errorRequest("wrong request: %s", cmd) + } + if element.Size() == 0 { + ok = false + } else { + num = getPositiveIndex(int(fkeys[0]), element.Size()) + key = strconv.Itoa(num) + value, ok = element.children[key] + } + } else { + key, _ = str(key) + num, err = strconv.Atoi(key) + if err != nil || element.Size() == 0 { + ok = false + err = nil + } else { + num = getPositiveIndex(num, element.Size()) + key = strconv.Itoa(num) + value, ok = element.children[key] + } + } + + } else if element.IsObject() { + key, _ = str(key) + value, ok = element.children[key] + } + if ok { + temporary = append(temporary, value) + ok = false + } + } + } + result = temporary + } + } + return +} + +// Eval evaluate expression `@.price == 19.95 && @.color == 'red'` to the result value i.e. Bool(true), Numeric(3.14), etc. +func Eval(node *Node, cmd string) (result *Node, err error) { + calc, err := newBuffer([]byte(cmd)).rpn() + if err != nil { + return nil, err + } + return eval(node, calc, cmd) +} + +func eval(node *Node, expression rpn, cmd string) (result *Node, err error) { + if node == nil { + return nil, nil + } + var ( + stack = make([]*Node, 0) + slice []*Node + temp *Node + fn Function + op Operation + ok bool + size int + commands []string + bstr []byte + ) + for _, exp := range expression { + size = len(stack) + if fn, ok = functions[exp]; ok { + if size < 1 { + return nil, errorRequest("wrong request: %s", cmd) + } + stack[size-1], err = fn(stack[size-1]) + if err != nil { + return + } + } else if op, ok = operations[exp]; ok { + if size < 2 { + return nil, errorRequest("wrong request: %s", cmd) + } + stack[size-2], err = op(stack[size-2], stack[size-1]) + if err != nil { + return + } + stack = stack[:size-1] + } else if len(exp) > 0 { + if exp[0] == dollar || exp[0] == at { + commands, err = ParseJSONPath(exp) + if err != nil { + return + } + slice, err = ApplyJSONPath(node, commands) + if err != nil { + return + } + if len(slice) > 1 { // array given + stack = append(stack, ArrayNode("", slice)) + } else if len(slice) == 1 { + stack = append(stack, slice[0]) + } else { // no data found + stack = append(stack, nil) + } + } else if constant, ok := constants[strings.ToLower(exp)]; ok { + stack = append(stack, constant) + } else { + bstr = []byte(exp) + size = len(bstr) + if size >= 2 && bstr[0] == quote && bstr[size-1] == quote { + if sstr, ok := unquote(bstr, quote); ok { + temp = StringNode("", sstr) + } else { + err = errorRequest("wrong request: %s", cmd) + } + } else { + temp, err = Unmarshal(bstr) + } + if err != nil { + return + } + stack = append(stack, temp) + } + } else { + stack = append(stack, valueNode(nil, "", String, "")) + } + } + if len(stack) == 1 { + if stack[0] == nil { + return NullNode(""), nil + } + return stack[0], nil + } + if len(stack) == 0 { + return NullNode(""), nil + } + return nil, errorRequest("wrong request: %s", cmd) +} + +func getNumberIndex(element *Node, input string, Default float64) (result float64, err error) { + var integer int + if input == "" { + result = Default + } else if input == "(@.length)" { + result = float64(element.Size()) + } else if strings.HasPrefix(input, "(") && strings.HasSuffix(input, ")") { + var expr rpn + var temp *Node + expr, err = newBuffer([]byte(input[1 : len(input)-1])).rpn() + if err != nil { + return 0, err + } + temp, err = eval(element, expr, input) + if err != nil { + return + } + integer, err = temp.getInteger() + if err != nil { + return + } + result = float64(integer) + } else { + integer, err = strconv.Atoi(input) + if err != nil { + return 0, err + } + result = float64(integer) + } + return +} + +func getPositiveIndex(index int, count int) int { + if index < 0 { + index += count + } + return index +} diff --git a/vendor/github.com/spyzhov/ajson/math.go b/vendor/github.com/spyzhov/ajson/math.go new file mode 100644 index 0000000..7087000 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/math.go @@ -0,0 +1,649 @@ +package ajson + +import ( + "encoding/base64" + "math" + "math/rand" + "regexp" + "sort" + "strings" +) + +// Function - internal left function of JSONPath +type Function func(node *Node) (result *Node, err error) + +// Operation - internal script operation of JSONPath +type Operation func(left *Node, right *Node) (result *Node, err error) + +var ( + // Operator precedence + // From https://golang.org/ref/spec#Operator_precedence + // + // Precedence Operator + // 5 * / % << >> & &^ + // 4 + - | ^ + // 3 == != < <= > >= =~ + // 2 && + // 1 || + // + // Arithmetic operators + // From https://golang.org/ref/spec#Arithmetic_operators + // + // + sum integers, floats, complex values, strings + // - difference integers, floats, complex values + // * product integers, floats, complex values + // / quotient integers, floats, complex values + // % remainder integers + // + // & bitwise AND integers + // | bitwise OR integers + // ^ bitwise XOR integers + // &^ bit clear (AND NOT) integers + // + // << left shift integer << unsigned integer + // >> right shift integer >> unsigned integer + // + // == equals any + // != not equals any + // < less any + // <= less or equals any + // > larger any + // >= larger or equals any + // =~ equals regex string strings + // + priority = map[string]uint8{ + "**": 6, // additional: power + "*": 5, + "/": 5, + "%": 5, + "<<": 5, + ">>": 5, + "&": 5, + "&^": 5, + "+": 4, + "-": 4, + "|": 4, + "^": 4, + "==": 3, + "!=": 3, + "<": 3, + "<=": 3, + ">": 3, + ">=": 3, + "=~": 3, + "&&": 2, + "||": 1, + } + priorityChar = map[byte]bool{ + '*': true, + '/': true, + '%': true, + '<': true, + '>': true, + '&': true, + '|': true, + '^': true, + '+': true, + '-': true, + '=': true, + '!': true, + } + + rightOp = map[string]bool{ + "**": true, + } + + operations = map[string]Operation{ + "**": func(left *Node, right *Node) (result *Node, err error) { + lnum, rnum, err := _floats(left, right) + if err != nil { + return + } + return valueNode(nil, "power", Numeric, math.Pow(lnum, rnum)), nil + }, + "*": func(left *Node, right *Node) (result *Node, err error) { + lnum, rnum, err := _floats(left, right) + if err != nil { + return + } + return valueNode(nil, "multiply", Numeric, float64(lnum*rnum)), nil + }, + "/": func(left *Node, right *Node) (result *Node, err error) { + lnum, rnum, err := _floats(left, right) + if err != nil { + return + } + if rnum == 0 { + return nil, errorRequest("division by zero") + } + return valueNode(nil, "division", Numeric, float64(lnum/rnum)), nil + }, + "%": func(left *Node, right *Node) (result *Node, err error) { + lnum, err := left.getInteger() + if err != nil { + return + } + rnum, err := right.getInteger() + if err != nil { + return + } + return valueNode(nil, "remainder", Numeric, float64(lnum%rnum)), nil + }, + "<<": func(left *Node, right *Node) (result *Node, err error) { + lnum, err := left.getInteger() + if err != nil { + return + } + rnum, err := right.getUInteger() + if err != nil { + return + } + return valueNode(nil, "left shift", Numeric, float64(lnum<>": func(left *Node, right *Node) (result *Node, err error) { + lnum, err := left.getInteger() + if err != nil { + return + } + rnum, err := right.getUInteger() + if err != nil { + return + } + return valueNode(nil, "right shift", Numeric, float64(lnum>>rnum)), nil + }, + "&": func(left *Node, right *Node) (result *Node, err error) { + lnum, rnum, err := _ints(left, right) + if err != nil { + return + } + return valueNode(nil, "bitwise AND", Numeric, float64(lnum&rnum)), nil + }, + "&^": func(left *Node, right *Node) (result *Node, err error) { + lnum, rnum, err := _ints(left, right) + if err != nil { + return + } + return valueNode(nil, "bit clear (AND NOT)", Numeric, float64(lnum&^rnum)), nil + }, + "+": func(left *Node, right *Node) (result *Node, err error) { + if left.IsString() { + lnum, rnum, err := _strings(left, right) + if err != nil { + return nil, err + } + return valueNode(nil, "sum", String, lnum+rnum), nil + } + lnum, rnum, err := _floats(left, right) + if err != nil { + return nil, err + } + return valueNode(nil, "sum", Numeric, float64(lnum+rnum)), nil + }, + "-": func(left *Node, right *Node) (result *Node, err error) { + lnum, rnum, err := _floats(left, right) + if err != nil { + return + } + return valueNode(nil, "sub", Numeric, float64(lnum-rnum)), nil + }, + "|": func(left *Node, right *Node) (result *Node, err error) { + lnum, rnum, err := _ints(left, right) + if err != nil { + return + } + return valueNode(nil, "bitwise OR", Numeric, float64(lnum|rnum)), nil + }, + "^": func(left *Node, right *Node) (result *Node, err error) { + lnum, rnum, err := _ints(left, right) + if err != nil { + return nil, err + } + return valueNode(nil, "bitwise XOR", Numeric, float64(lnum^rnum)), nil + }, + "==": func(left *Node, right *Node) (result *Node, err error) { + if left == nil || right == nil { + return valueNode(nil, "eq", Bool, false), nil + } + res, err := left.Eq(right) + if err != nil { + return nil, err + } + return valueNode(nil, "eq", Bool, res), nil + }, + "!=": func(left *Node, right *Node) (result *Node, err error) { + if left == nil || right == nil { + return valueNode(nil, "neq", Bool, false), nil + } + res, err := left.Eq(right) + if err != nil { + return nil, err + } + return valueNode(nil, "neq", Bool, !res), nil + }, + "=~": func(left *Node, right *Node) (node *Node, err error) { + pattern, err := right.GetString() + if err != nil { + return nil, err + } + val, err := left.GetString() + if err != nil { + return nil, err + } + res, err := regexp.MatchString(pattern, val) + if err != nil { + return nil, err + } + return valueNode(nil, "eq", Bool, res), nil + }, + "<": func(left *Node, right *Node) (result *Node, err error) { + if left == nil || right == nil { + return valueNode(nil, "le", Bool, false), nil + } + res, err := left.Le(right) + if err != nil { + return nil, err + } + return valueNode(nil, "le", Bool, bool(res)), nil + }, + "<=": func(left *Node, right *Node) (result *Node, err error) { + if left == nil || right == nil { + return valueNode(nil, "leq", Bool, false), nil + } + res, err := left.Leq(right) + if err != nil { + return nil, err + } + return valueNode(nil, "leq", Bool, bool(res)), nil + }, + ">": func(left *Node, right *Node) (result *Node, err error) { + if left == nil || right == nil { + return valueNode(nil, "ge", Bool, false), nil + } + res, err := left.Ge(right) + if err != nil { + return nil, err + } + return valueNode(nil, "ge", Bool, bool(res)), nil + }, + ">=": func(left *Node, right *Node) (result *Node, err error) { + if left == nil || right == nil { + return valueNode(nil, "geq", Bool, false), nil + } + res, err := left.Geq(right) + if err != nil { + return nil, err + } + return valueNode(nil, "geq", Bool, bool(res)), nil + }, + "&&": func(left *Node, right *Node) (result *Node, err error) { + res := false + lval, err := boolean(left) + if err != nil { + return nil, err + } + if lval { + rval, err := boolean(right) + if err != nil { + return nil, err + } + res = rval + } + return valueNode(nil, "AND", Bool, bool(res)), nil + }, + "||": func(left *Node, right *Node) (result *Node, err error) { + res := true + lval, err := boolean(left) + if err != nil { + return nil, err + } + if !lval { + rval, err := boolean(right) + if err != nil { + return nil, err + } + res = rval + } + return valueNode(nil, "OR", Bool, bool(res)), nil + }, + } + + randFunc = rand.Float64 + randIntFunc = rand.Intn + + functions = map[string]Function{ + "abs": numericFunction("Abs", math.Abs), + "acos": numericFunction("Acos", math.Acos), + "acosh": numericFunction("Acosh", math.Acosh), + "asin": numericFunction("Asin", math.Asin), + "asinh": numericFunction("Asinh", math.Asinh), + "atan": numericFunction("Atan", math.Atan), + "atanh": numericFunction("Atanh", math.Atanh), + "cbrt": numericFunction("Cbrt", math.Cbrt), + "ceil": numericFunction("Ceil", math.Ceil), + "cos": numericFunction("Cos", math.Cos), + "cosh": numericFunction("Cosh", math.Cosh), + "erf": numericFunction("Erf", math.Erf), + "erfc": numericFunction("Erfc", math.Erfc), + "erfcinv": numericFunction("Erfcinv", math.Erfcinv), + "erfinv": numericFunction("Erfinv", math.Erfinv), + "exp": numericFunction("Exp", math.Exp), + "exp2": numericFunction("Exp2", math.Exp2), + "expm1": numericFunction("Expm1", math.Expm1), + "floor": numericFunction("Floor", math.Floor), + "gamma": numericFunction("Gamma", math.Gamma), + "j0": numericFunction("J0", math.J0), + "j1": numericFunction("J1", math.J1), + "log": numericFunction("Log", math.Log), + "log10": numericFunction("Log10", math.Log10), + "log1p": numericFunction("Log1p", math.Log1p), + "log2": numericFunction("Log2", math.Log2), + "logb": numericFunction("Logb", math.Logb), + "round": numericFunction("Round", math.Round), + "roundtoeven": numericFunction("RoundToEven", math.RoundToEven), + "sin": numericFunction("Sin", math.Sin), + "sinh": numericFunction("Sinh", math.Sinh), + "sqrt": numericFunction("Sqrt", math.Sqrt), + "tan": numericFunction("Tan", math.Tan), + "tanh": numericFunction("Tanh", math.Tanh), + "trunc": numericFunction("Trunc", math.Trunc), + "y0": numericFunction("Y0", math.Y0), + "y1": numericFunction("Y1", math.Y1), + + "pow10": func(node *Node) (result *Node, err error) { + if node == nil { + return valueNode(nil, "Pow10", Numeric, 0), nil + } + num, err := node.getInteger() + if err != nil { + return + } + return valueNode(nil, "Pow10", Numeric, float64(math.Pow10(num))), nil + }, + "length": func(node *Node) (result *Node, err error) { + if node == nil { + return valueNode(nil, "length", Numeric, float64(0)), nil + } + if node.IsArray() { + return valueNode(nil, "length", Numeric, float64(node.Size())), nil + } + if node.IsString() { + if res, err := node.GetString(); err != nil { + return nil, err + } else { + return valueNode(nil, "length", Numeric, float64(len(res))), nil + } + } + return valueNode(nil, "length", Numeric, float64(1)), nil + }, + "size": func(node *Node) (result *Node, err error) { + return valueNode(nil, "size", Numeric, float64(node.Size())), nil + }, + "factorial": func(node *Node) (result *Node, err error) { + if node == nil { + return valueNode(nil, "factorial", Numeric, 0), nil + } + num, err := node.getUInteger() + if err != nil { + return + } + return valueNode(nil, "factorial", Numeric, float64(mathFactorial(num))), nil + }, + "avg": func(node *Node) (result *Node, err error) { + if node == nil { + return valueNode(nil, "avg", Null, nil), nil + } + if node.isContainer() { + sum := float64(0) + if node.Size() == 0 { + return valueNode(nil, "avg", Numeric, sum), nil + } + var value float64 + for _, temp := range node.Inheritors() { + value, err = temp.GetNumeric() + if err != nil { + return nil, err + } + sum += value + } + return valueNode(nil, "avg", Numeric, sum/float64(node.Size())), nil + } + if node.IsNumeric() { + value, err := node.GetNumeric() + if err != nil { + return nil, err + } + return valueNode(nil, "avg", Numeric, value), nil + } + return valueNode(nil, "avg", Null, nil), nil + }, + "b64decode": func(node *Node) (result *Node, err error) { + if node.IsString() { + if sourceString, err := node.GetString(); err != nil { + return nil, err + } else { + var result []byte + result, err = base64.StdEncoding.WithPadding(base64.StdPadding).DecodeString(sourceString) + if err != nil { + // then for NO_PAD encoded strings, if the first result with error + result, err = base64.StdEncoding.WithPadding(base64.NoPadding).DecodeString(sourceString) + } + if err != nil { + return nil, err + } + return valueNode(nil, "b64decode", String, string(result)), nil + } + } + return valueNode(nil, "b64decode", Null, nil), nil + }, + "b64encoden": func(node *Node) (result *Node, err error) { + if node.IsString() { + if sourceString, err := node.GetString(); err != nil { + return nil, err + } else { + remainder := len(sourceString) % 3 + size := len(sourceString) / 3 * 4 + if remainder != 0 { + size += 1 + remainder + } + var result []byte = make([]byte, size) + base64.StdEncoding.WithPadding(base64.NoPadding).Encode(result, []byte(sourceString)) + return valueNode(nil, "b64encoden", String, string(result)), nil + } + } + return valueNode(nil, "b64encoden", Null, nil), nil + }, + "b64encode": func(node *Node) (result *Node, err error) { + if node.IsString() { + if sourceString, err := node.GetString(); err != nil { + return nil, err + } else { + remainder := len(sourceString) % 3 + size := len(sourceString) / 3 * 4 + if remainder != 0 { + size += 4 + } + var result []byte = make([]byte, size) + base64.StdEncoding.WithPadding(base64.StdPadding).Encode(result, []byte(sourceString)) + return valueNode(nil, "b64encode", String, string(result)), nil + } + } + return valueNode(nil, "b64encode", Null, nil), nil + }, + "sum": func(node *Node) (result *Node, err error) { + if node == nil { + return valueNode(nil, "sum", Null, nil), nil + } + if node.isContainer() { + sum := float64(0) + if node.Size() == 0 { + return valueNode(nil, "sum", Numeric, sum), nil + } + var value float64 + for _, temp := range node.Inheritors() { + value, err = temp.GetNumeric() + if err != nil { + return nil, err + } + sum += value + } + return valueNode(nil, "sum", Numeric, sum), nil + } + if node.IsNumeric() { + value, err := node.GetNumeric() + if err != nil { + return nil, err + } + return valueNode(nil, "sum", Numeric, value), nil + } + return valueNode(nil, "sum", Null, nil), nil + }, + "not": func(node *Node) (result *Node, err error) { + if value, err := boolean(node); err != nil { + return nil, err + } else { + return valueNode(nil, "not", Bool, !value), nil + } + }, + "rand": func(node *Node) (result *Node, err error) { + if node == nil { + return nil, errorType() + } + num, err := node.GetNumeric() + if err != nil { + return + } + return valueNode(nil, "Rand", Numeric, randFunc()*num), nil + }, + "randint": func(node *Node) (result *Node, err error) { + if node == nil { + return nil, errorType() + } + num, err := node.getInteger() + if err != nil { + return + } + return valueNode(nil, "RandInt", Numeric, float64(randIntFunc(num))), nil + }, + "last": func(node *Node) (result *Node, err error) { + if node.IsArray() { + array := node.Inheritors() + if len(array) > 0 { + return array[len(array)-1], nil + } + } + return valueNode(nil, "last", Null, nil), nil + }, + "first": func(node *Node) (result *Node, err error) { + if node.IsArray() { + array := node.Inheritors() + if len(array) > 0 { + return array[0], nil + } + } + return valueNode(nil, "first", Null, nil), nil + }, + "parent": func(node *Node) (result *Node, err error) { + if node == nil { + return valueNode(nil, "parent", Null, nil), nil + } + if node.parent != nil { + return node.parent, nil + } + return valueNode(nil, "parent", Null, nil), nil + }, + "root": func(node *Node) (result *Node, err error) { + if node == nil { + return valueNode(nil, "root", Null, nil), nil + } + root := node.root() + if root != nil { + return root, nil + } + return valueNode(nil, "root", Null, nil), nil + }, + "key": func(node *Node) (result *Node, err error) { + if node == nil { + return valueNode(nil, "key", Null, nil), nil + } + if node.parent != nil { + if node.parent.IsObject() { + return valueNode(nil, "key", String, node.Key()), nil + } + } + return valueNode(nil, "key", Null, nil), nil + }, + } + + constants = map[string]*Node{ + "e": valueNode(nil, "e", Numeric, float64(math.E)), + "pi": valueNode(nil, "pi", Numeric, float64(math.Pi)), + "phi": valueNode(nil, "phi", Numeric, float64(math.Phi)), + + "sqrt2": valueNode(nil, "sqrt2", Numeric, float64(math.Sqrt2)), + "sqrte": valueNode(nil, "sqrte", Numeric, float64(math.SqrtE)), + "sqrtpi": valueNode(nil, "sqrtpi", Numeric, float64(math.SqrtPi)), + "sqrtphi": valueNode(nil, "sqrtphi", Numeric, float64(math.SqrtPhi)), + + "ln2": valueNode(nil, "ln2", Numeric, float64(math.Ln2)), + "log2e": valueNode(nil, "log2e", Numeric, float64(math.Log2E)), + "ln10": valueNode(nil, "ln10", Numeric, float64(math.Ln10)), + "log10e": valueNode(nil, "log10e", Numeric, float64(math.Log10E)), + + "true": valueNode(nil, "true", Bool, true), + "false": valueNode(nil, "false", Bool, false), + "null": valueNode(nil, "null", Null, nil), + } +) + +// AddFunction add a function for internal JSONPath script +func AddFunction(alias string, function Function) { + functions[strings.ToLower(alias)] = function +} + +// AddOperation add an operation for internal JSONPath script +func AddOperation(alias string, prior uint8, right bool, operation Operation) { + alias = strings.ToLower(alias) + operations[alias] = operation + priority[alias] = prior + priorityChar[alias[0]] = true + if right { + rightOp[alias] = true + } +} + +// AddConstant add a constant for internal JSONPath script +func AddConstant(alias string, value *Node) { + constants[strings.ToLower(alias)] = value +} + +func numericFunction(name string, fn func(float float64) float64) Function { + return func(node *Node) (result *Node, err error) { + if node.IsNumeric() { + num, err := node.GetNumeric() + if err != nil { + return nil, err + } + return valueNode(nil, name, Numeric, fn(num)), nil + } + return nil, errorRequest("function '%s' was called from non numeric node", name) + } +} + +func mathFactorial(x uint) uint { + if x == 0 { + return 1 + } + return x * mathFactorial(x-1) +} + +func comparisonOperationsOrder() []string { + result := make([]string, 0, len(operations)) + for operation := range operations { + result = append(result, operation) + } + + sort.Slice(result, func(i, j int) bool { + return len(result[i]) > len(result[j]) + }) + return result +} diff --git a/vendor/github.com/spyzhov/ajson/node.go b/vendor/github.com/spyzhov/ajson/node.go new file mode 100644 index 0000000..baca667 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/node.go @@ -0,0 +1,949 @@ +package ajson + +import ( + "math" + "sort" + "strconv" + "sync/atomic" +) + +// Node is a main struct, presents any type of JSON node. +// Available types are: +// +// const ( +// Null NodeType = iota +// Numeric +// String +// Bool +// Array +// Object +// ) +// +// Every type has its own methods to be called. +// Every Node contains link to a byte data, parent and children, also calculated type of value, atomic value and internal information. +type Node struct { + parent *Node + children map[string]*Node + key *string + index *int + _type NodeType + data *[]byte + borders [2]int + value atomic.Value + dirty bool +} + +// NodeType is a kind of reflection of JSON type to a type of golang +type NodeType int32 + +// Reflections: +// +// Null = nil.(interface{}) +// Numeric = float64 +// String = string +// Bool = bool +// Array = []*Node +// Object = map[string]*Node +// +const ( + // Null is reflection of nil.(interface{}) + Null NodeType = iota + // Numeric is reflection of float64 + Numeric + // String is reflection of string + String + // Bool is reflection of bool + Bool + // Array is reflection of []*Node + Array + // Object is reflection of map[string]*Node + Object +) + +// NullNode is constructor for Node with Null value +func NullNode(key string) *Node { + return &Node{ + _type: Null, + key: &key, + dirty: true, + } +} + +// NumericNode is constructor for Node with a Numeric value +func NumericNode(key string, value float64) (current *Node) { + current = &Node{ + _type: Numeric, + key: &key, + dirty: true, + } + current.value.Store(value) + return +} + +// StringNode is constructor for Node with a String value +func StringNode(key string, value string) (current *Node) { + current = &Node{ + _type: String, + key: &key, + dirty: true, + } + current.value.Store(value) + return +} + +// BoolNode is constructor for Node with a Bool value +func BoolNode(key string, value bool) (current *Node) { + current = &Node{ + _type: Bool, + key: &key, + dirty: true, + } + current.value.Store(value) + return +} + +// ArrayNode is constructor for Node with an Array value +func ArrayNode(key string, value []*Node) (current *Node) { + current = &Node{ + data: nil, + _type: Array, + key: &key, + dirty: true, + } + current.children = make(map[string]*Node, len(value)) + if value != nil { + for i := range value { + var index = i + current.children[strconv.Itoa(index)] = value[index] + value[index].parent = current + value[index].index = &index + } + current.value.Store(value) + } + return +} + +// ObjectNode is constructor for Node with an Object value +func ObjectNode(key string, value map[string]*Node) (current *Node) { + current = &Node{ + _type: Object, + key: &key, + children: value, + dirty: true, + } + if value != nil { + for key, val := range value { + vkey := key + val.parent = current + val.key = &vkey + } + current.value.Store(value) + } else { + current.children = make(map[string]*Node) + } + return +} + +func newNode(parent *Node, buf *buffer, _type NodeType, key **string) (current *Node, err error) { + current = &Node{ + parent: parent, + data: &buf.data, + borders: [2]int{buf.index, 0}, + _type: _type, + key: *key, + dirty: false, + } + if _type == Object || _type == Array { + current.children = make(map[string]*Node) + } + if parent != nil { + if parent.IsArray() { + size := len(parent.children) + current.index = &size + parent.children[strconv.Itoa(size)] = current + } else if parent.IsObject() { + if *key == nil { + err = errorSymbol(buf) + } else { + parent.children[**key] = current + } + } else { + err = errorSymbol(buf) + } + } + return +} + +func valueNode(parent *Node, key string, _type NodeType, value interface{}) (current *Node) { + current = &Node{ + parent: parent, + data: nil, + borders: [2]int{0, 0}, + _type: _type, + key: &key, + dirty: true, + } + if value != nil { + current.value.Store(value) + } + return +} + +// Parent returns link to the parent of current node, nil for root +func (n *Node) Parent() *Node { + if n == nil { + return nil + } + return n.parent +} + +// Source returns slice of bytes, which was identified to be current node +func (n *Node) Source() []byte { + if n == nil { + return nil + } + if n.ready() && !n.dirty && n.data != nil { + return (*n.data)[n.borders[0]:n.borders[1]] + } + return nil +} + +// String is implementation of Stringer interface, returns string based on source part +func (n *Node) String() string { + if n == nil { + return "" + } + if n.ready() && !n.dirty { + return string(n.Source()) + } + val, err := Marshal(n) + if err != nil { + return "Error: " + err.Error() + } + return string(val) +} + +// Type will return type of current node +func (n *Node) Type() NodeType { + if n == nil { + return Null + } + return n._type +} + +// Key will return key of current node, please check, that parent of this node has an Object type +func (n *Node) Key() string { + if n == nil { + return "" + } + if n.key == nil { + return "" + } + return *n.key +} + +// Index will return index of current node, please check, that parent of this node has an Array type +func (n *Node) Index() int { + if n == nil { + return -1 + } + if n.index == nil { + return -1 + } + return *n.index +} + +// Size will return count of children of current node, please check, that parent of this node has an Array type +func (n *Node) Size() int { + if n == nil { + return 0 + } + return len(n.children) +} + +// Keys will return count all keys of children of current node, please check, that parent of this node has an Object type +func (n *Node) Keys() (result []string) { + if n == nil { + return nil + } + result = make([]string, 0, len(n.children)) + for key := range n.children { + result = append(result, key) + } + return +} + +// IsArray returns true if current node is Array +func (n *Node) IsArray() bool { + if n == nil { + return false + } + return n._type == Array +} + +// IsObject returns true if current node is Object +func (n *Node) IsObject() bool { + if n == nil { + return false + } + return n._type == Object +} + +// IsNull returns true if current node is Null +func (n *Node) IsNull() bool { + if n == nil { + return false + } + return n._type == Null +} + +// IsNumeric returns true if current node is Numeric +func (n *Node) IsNumeric() bool { + if n == nil { + return false + } + return n._type == Numeric +} + +// IsString returns true if current node is String +func (n *Node) IsString() bool { + if n == nil { + return false + } + return n._type == String +} + +// IsBool returns true if current node is Bool +func (n *Node) IsBool() bool { + if n == nil { + return false + } + return n._type == Bool +} + +// Value is calculating and returns a value of current node. +// +// It returns nil, if current node type is Null. +// +// It returns float64, if current node type is Numeric. +// +// It returns string, if current node type is String. +// +// It returns bool, if current node type is Bool. +// +// It returns []*Node, if current node type is Array. +// +// It returns map[string]*Node, if current node type is Object. +// +// BUT! Current method doesn't calculate underlying nodes (use method Node.Unpack for that). +// +// Value will be calculated only once and saved into atomic.Value. +func (n *Node) Value() (value interface{}, err error) { + if n == nil { + return nil, errorUnparsed() + } + switch n._type { + case Null: + return n.GetNull() + case Numeric: + return n.GetNumeric() + case String: + return n.GetString() + case Bool: + return n.GetBool() + case Array: + return n.GetArray() + case Object: + return n.GetObject() + } + return nil, errorType() +} + +func (n *Node) getValue() (value interface{}, err error) { + value = n.value.Load() + if value == nil { + switch n._type { + case Null: + return nil, nil + case Numeric: + value, err = strconv.ParseFloat(string(n.Source()), 64) + if err != nil { + return + } + n.value.Store(value) + case String: + var ok bool + value, ok = unquote(n.Source(), quotes) + if !ok { + return "", errorAt(n.borders[0], (*n.data)[n.borders[0]]) + } + n.value.Store(value) + case Bool: + if len(n.Source()) == 0 { + return nil, errorUnparsed() + } + b := n.Source()[0] + value = b == 't' || b == 'T' + n.value.Store(value) + case Array: + children := make([]*Node, len(n.children)) + for _, child := range n.children { + children[*child.index] = child + } + value = children + n.value.Store(value) + case Object: + result := make(map[string]*Node) + for key, child := range n.children { + result[key] = child + } + value = result + n.value.Store(value) + } + } + return +} + +// GetNull returns nil, if current type is Null, else: WrongType error +func (n *Node) GetNull() (interface{}, error) { + if n == nil { + return nil, errorUnparsed() + } + if n._type != Null { + return nil, errorType() + } + return nil, nil +} + +// GetNumeric returns float64, if current type is Numeric, else: WrongType error +func (n *Node) GetNumeric() (value float64, err error) { + if n == nil { + return 0, errorUnparsed() + } + if n._type != Numeric { + return value, errorType() + } + iValue, err := n.getValue() + if err != nil { + return 0, err + } + value, ok := iValue.(float64) + if !ok { + return value, errorType() + } + return value, nil +} + +// GetString returns string, if current type is String, else: WrongType error +func (n *Node) GetString() (value string, err error) { + if n == nil { + return "", errorUnparsed() + } + if n._type != String { + return value, errorType() + } + iValue, err := n.getValue() + if err != nil { + return "", err + } + value, ok := iValue.(string) + if !ok { + return value, errorType() + } + return value, nil +} + +// GetBool returns bool, if current type is Bool, else: WrongType error +func (n *Node) GetBool() (value bool, err error) { + if n == nil { + return value, errorUnparsed() + } + if n._type != Bool { + return value, errorType() + } + iValue, err := n.getValue() + if err != nil { + return false, err + } + value, ok := iValue.(bool) + if !ok { + return value, errorType() + } + return value, nil +} + +// GetArray returns []*Node, if current type is Array, else: WrongType error +func (n *Node) GetArray() (value []*Node, err error) { + if n == nil { + return nil, errorUnparsed() + } + if n._type != Array { + return value, errorType() + } + iValue, err := n.getValue() + if err != nil { + return nil, err + } + value, ok := iValue.([]*Node) + if !ok { + return value, errorType() + } + return value, nil +} + +// GetObject returns map[string]*Node, if current type is Object, else: WrongType error +func (n *Node) GetObject() (value map[string]*Node, err error) { + if n == nil { + return nil, errorUnparsed() + } + if n._type != Object { + return value, errorType() + } + iValue, err := n.getValue() + if err != nil { + return nil, err + } + value, ok := iValue.(map[string]*Node) + if !ok { + return value, errorType() + } + return value, nil +} + +// MustNull returns nil, if current type is Null, else: panic if error happened +func (n *Node) MustNull() (value interface{}) { + value, err := n.GetNull() + if err != nil { + panic(err) + } + return +} + +// MustNumeric returns float64, if current type is Numeric, else: panic if error happened +func (n *Node) MustNumeric() (value float64) { + value, err := n.GetNumeric() + if err != nil { + panic(err) + } + return +} + +// MustString returns string, if current type is String, else: panic if error happened +func (n *Node) MustString() (value string) { + value, err := n.GetString() + if err != nil { + panic(err) + } + return +} + +// MustBool returns bool, if current type is Bool, else: panic if error happened +func (n *Node) MustBool() (value bool) { + value, err := n.GetBool() + if err != nil { + panic(err) + } + return +} + +// MustArray returns []*Node, if current type is Array, else: panic if error happened +func (n *Node) MustArray() (value []*Node) { + value, err := n.GetArray() + if err != nil { + panic(err) + } + return +} + +// MustObject returns map[string]*Node, if current type is Object, else: panic if error happened +func (n *Node) MustObject() (value map[string]*Node) { + value, err := n.GetObject() + if err != nil { + panic(err) + } + return +} + +// Unpack will produce current node to it's interface, recursively with all underlying nodes (in contrast to Node.Value). +func (n *Node) Unpack() (value interface{}, err error) { + if n == nil { + return nil, errorUnparsed() + } + switch n._type { + case Null: + return nil, nil + case Numeric: + value, err = n.Value() + if _, ok := value.(float64); !ok { + return nil, errorType() + } + case String: + value, err = n.Value() + if _, ok := value.(string); !ok { + return nil, errorType() + } + case Bool: + value, err = n.Value() + if _, ok := value.(bool); !ok { + return nil, errorType() + } + case Array: + children := make([]interface{}, len(n.children)) + for _, child := range n.children { + val, err := child.Unpack() + if err != nil { + return nil, err + } + children[*child.index] = val + } + value = children + case Object: + result := make(map[string]interface{}) + for key, child := range n.children { + result[key], err = child.Unpack() + if err != nil { + return nil, err + } + } + value = result + } + return +} + +// GetIndex will return child node of current array node. If current node is not Array, or index is unavailable, will return error +func (n *Node) GetIndex(index int) (*Node, error) { + if n == nil { + return nil, errorUnparsed() + } + if n._type != Array { + return nil, errorType() + } + if index < 0 { + index += len(n.children) + } + child, ok := n.children[strconv.Itoa(index)] + if !ok { + return nil, errorRequest("out of index %d", index) + } + return child, nil +} + +// MustIndex will return child node of current array node. If current node is not Array, or index is unavailable, raise a panic +func (n *Node) MustIndex(index int) (value *Node) { + value, err := n.GetIndex(index) + if err != nil { + panic(err) + } + return +} + +// GetKey will return child node of current object node. If current node is not Object, or key is unavailable, will return error +func (n *Node) GetKey(key string) (*Node, error) { + if n == nil { + return nil, errorUnparsed() + } + if n._type != Object { + return nil, errorType() + } + value, ok := n.children[key] + if !ok { + return nil, errorRequest("wrong key '%s'", key) + } + return value, nil +} + +// MustKey will return child node of current object node. If current node is not Object, or key is unavailable, raise a panic +func (n *Node) MustKey(key string) (value *Node) { + value, err := n.GetKey(key) + if err != nil { + panic(err) + } + return +} + +// HasKey will return boolean value, if current object node has custom key +func (n *Node) HasKey(key string) bool { + if n == nil { + return false + } + _, ok := n.children[key] + return ok +} + +// Empty method check if current container node has no children +func (n *Node) Empty() bool { + if n == nil { + return false + } + return len(n.children) == 0 +} + +// Path returns full JsonPath of current Node +func (n *Node) Path() string { + if n == nil { + return "" + } + if n.parent == nil { + return "$" + } + if n.key != nil { + return n.parent.Path() + "['" + n.Key() + "']" + } + return n.parent.Path() + "[" + strconv.Itoa(n.Index()) + "]" +} + +// Eq check if nodes value are the same +func (n *Node) Eq(node *Node) (result bool, err error) { + if n == nil || node == nil { + return false, errorUnparsed() + } + if n.Type() == node.Type() { + switch n.Type() { + case Bool: + lnum, rnum, err := _bools(n, node) + if err != nil { + return false, err + } + result = lnum == rnum + case Numeric: + lnum, rnum, err := _floats(n, node) + if err != nil { + return false, err + } + result = lnum == rnum + case String: + lnum, rnum, err := _strings(n, node) + if err != nil { + return false, err + } + result = lnum == rnum + case Null: + // Null type always is the same + result = true + case Array: + lnum, rnum, err := _arrays(n, node) + if err != nil { + return false, err + } + if len(lnum) == len(rnum) { + result = true + for i := range lnum { + result, err = lnum[i].Eq(rnum[i]) + if err != nil { + return false, err + } + if !result { + return false, err + } + } + } + case Object: + lnum, rnum, err := _objects(n, node) + if err != nil { + return false, err + } + if len(lnum) == len(rnum) { + result = true + for i := range lnum { + element, ok := rnum[i] + if !ok { + return false, nil + } + result, err = lnum[i].Eq(element) + if err != nil { + return false, err + } + if !result { + return false, err + } + } + } + } + } + return +} + +// Neq check if nodes value are not the same +func (n *Node) Neq(node *Node) (result bool, err error) { + result, err = n.Eq(node) + return !result, err +} + +// Le check if nodes value is lesser than given +func (n *Node) Le(node *Node) (result bool, err error) { + if n == nil || node == nil { + return false, errorUnparsed() + } + if n.Type() == node.Type() { + switch n.Type() { + case Numeric: + lnum, rnum, err := _floats(n, node) + if err != nil { + return false, err + } + result = lnum < rnum + case String: + lnum, rnum, err := _strings(n, node) + if err != nil { + return false, err + } + result = lnum < rnum + default: + return false, errorType() + } + } + return +} + +// Leq check if nodes value is lesser or equal than given +func (n *Node) Leq(node *Node) (result bool, err error) { + if n == nil || node == nil { + return false, errorUnparsed() + } + if n.Type() == node.Type() { + switch n.Type() { + case Numeric: + lnum, rnum, err := _floats(n, node) + if err != nil { + return false, err + } + result = lnum <= rnum + case String: + lnum, rnum, err := _strings(n, node) + if err != nil { + return false, err + } + result = lnum <= rnum + default: + return false, errorType() + } + } + return +} + +// Ge check if nodes value is greater than given +func (n *Node) Ge(node *Node) (result bool, err error) { + if n == nil || node == nil { + return false, errorUnparsed() + } + if n.Type() == node.Type() { + switch n.Type() { + case Numeric: + lnum, rnum, err := _floats(n, node) + if err != nil { + return false, err + } + result = lnum > rnum + case String: + lnum, rnum, err := _strings(n, node) + if err != nil { + return false, err + } + result = lnum > rnum + default: + return false, errorType() + } + } + return +} + +// Geq check if nodes value is greater or equal than given +func (n *Node) Geq(node *Node) (result bool, err error) { + if n == nil || node == nil { + return false, errorUnparsed() + } + if n.Type() == node.Type() { + switch n.Type() { + case Numeric: + lnum, rnum, err := _floats(n, node) + if err != nil { + return false, err + } + result = lnum >= rnum + case String: + lnum, rnum, err := _strings(n, node) + if err != nil { + return false, err + } + result = lnum >= rnum + default: + return false, errorType() + } + } + return +} + +func (n *Node) ready() bool { + return n.borders[1] != 0 +} + +func (n *Node) isContainer() bool { + return n._type == Array || n._type == Object +} + +func (n *Node) getInteger() (int, error) { + if !n.IsNumeric() { + return 0, errorType() + } + float, err := n.GetNumeric() + if err != nil { + return 0, err + } + if math.Mod(float, 1.0) != 0 { + return 0, errorRequest("node is not INT") + } + return int(float), nil +} + +func (n *Node) getUInteger() (uint, error) { + result, err := n.getInteger() + if err != nil { + return 0, err + } + if result < 0 { + return 0, errorRequest("node is not UINT") + } + return uint(result), nil +} + +// Inheritors return sorted by keys/index slice of children +func (n *Node) Inheritors() (result []*Node) { + if n == nil { + return nil + } + size := len(n.children) + if n.IsObject() { + result = make([]*Node, size) + keys := n.Keys() + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + for i, key := range keys { + result[i] = n.children[key] + } + } else if n.IsArray() { + result = make([]*Node, size) + for _, element := range n.children { + result[*element.index] = element + } + } + return +} + +// JSONPath evaluate path for current node +func (n *Node) JSONPath(path string) (result []*Node, err error) { + commands, err := ParseJSONPath(path) + if err != nil { + return nil, err + } + return ApplyJSONPath(n, commands) +} + +// root returns the root node +func (n *Node) root() (node *Node) { + node = n + for node.parent != nil { + node = node.parent + } + return node +} diff --git a/vendor/github.com/spyzhov/ajson/node_mutations.go b/vendor/github.com/spyzhov/ajson/node_mutations.go new file mode 100644 index 0000000..0d5bf72 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/node_mutations.go @@ -0,0 +1,378 @@ +package ajson + +import ( + "strconv" + "sync/atomic" +) + +// IsDirty is the flag that shows, was node changed or not +func (n *Node) IsDirty() bool { + return n.dirty +} + +// Set updates current node value with the value of any type +func (n *Node) Set(value interface{}) error { + if value == nil { + return n.SetNull() + } + switch result := value.(type) { + case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + if tValue, err := numeric2float64(value); err != nil { + return err + } else { + return n.SetNumeric(tValue) + } + case string: + return n.SetString(result) + case bool: + return n.SetBool(result) + case []*Node: + return n.SetArray(result) + case map[string]*Node: + return n.SetObject(result) + case *Node: + return n.SetNode(result) + default: + return unsupportedType(value) + } +} + +// SetNull updates current node value with Null value +func (n *Node) SetNull() error { + return n.update(Null, nil) +} + +// SetNumeric updates current node value with Numeric value +func (n *Node) SetNumeric(value float64) error { + return n.update(Numeric, value) +} + +// SetString updates current node value with String value +func (n *Node) SetString(value string) error { + return n.update(String, value) +} + +// SetBool updates current node value with Bool value +func (n *Node) SetBool(value bool) error { + return n.update(Bool, value) +} + +// SetArray updates current node value with Array value +func (n *Node) SetArray(value []*Node) error { + return n.update(Array, value) +} + +// SetObject updates current node value with Object value +func (n *Node) SetObject(value map[string]*Node) error { + return n.update(Object, value) +} + +// SetNode updates current node value with the clone of the given Node value +// NB! The result will be the clone of the given Node! +func (n *Node) SetNode(value *Node) error { + if n == value { + // Attempt to set current node as the value: node.SetNode(node) + return nil + } + if n.isParentOrSelfNode(value) { + return errorRequest("attempt to create infinite loop") + } + + node := value.Clone() + node.setReference(n.parent, n.key, n.index) + n.setReference(nil, nil, nil) + *n = *node + if n.parent != nil { + n.parent.mark() + } + return nil +} + +// AppendArray appends current Array node values with Node values +func (n *Node) AppendArray(value ...*Node) error { + if !n.IsArray() { + return errorType() + } + for _, val := range value { + if err := n.appendNode(nil, val); err != nil { + return err + } + } + n.mark() + return nil +} + +// AppendObject appends current Object node value with key:value +func (n *Node) AppendObject(key string, value *Node) error { + if !n.IsObject() { + return errorType() + } + err := n.appendNode(&key, value) + if err != nil { + return err + } + n.mark() + return nil +} + +// DeleteNode removes element child +func (n *Node) DeleteNode(value *Node) error { + return n.remove(value) +} + +// DeleteKey removes element from Object, by it's key +func (n *Node) DeleteKey(key string) error { + node, err := n.GetKey(key) + if err != nil { + return err + } + return n.remove(node) +} + +// PopKey removes element from Object, by it's key and return it +func (n *Node) PopKey(key string) (node *Node, err error) { + node, err = n.GetKey(key) + if err != nil { + return + } + return node, n.remove(node) +} + +// DeleteIndex removes element from Array, by it's index +func (n *Node) DeleteIndex(index int) error { + node, err := n.GetIndex(index) + if err != nil { + return err + } + return n.remove(node) +} + +// PopIndex removes element from Array, by it's index and return it +func (n *Node) PopIndex(index int) (node *Node, err error) { + node, err = n.GetIndex(index) + if err != nil { + return + } + return node, n.remove(node) +} + +// Delete removes element from parent. For root - do nothing. +func (n *Node) Delete() error { + if n.parent == nil { + return nil + } + return n.parent.remove(n) +} + +// Clone creates full copy of current Node. With all child, but without link to the parent. +func (n *Node) Clone() *Node { + node := n.clone() + node.setReference(nil, nil, nil) + return node +} + +func (n *Node) clone() *Node { + node := &Node{ + parent: n.parent, + children: make(map[string]*Node, len(n.children)), + key: cptrs(n.key), + index: cptri(n.index), + _type: n._type, + data: n.data, + borders: n.borders, + value: n.value, + dirty: n.dirty, + } + for key, value := range n.children { + clone := value.clone() + clone.parent = node + node.children[key] = clone + } + return node +} + +// update method updates stored value, with validations +func (n *Node) update(_type NodeType, value interface{}) error { + // validate + err := n.validate(_type, value) + if err != nil { + return err + } + // update + n.mark() + n.clear() + + atomic.StoreInt32((*int32)(&n._type), int32(_type)) + n.value = atomic.Value{} + if value != nil { + switch _type { + case Array: + nodes := value.([]*Node) + n.children = make(map[string]*Node, len(nodes)) + for _, node := range nodes { + tnode := node + if err = n.appendNode(nil, tnode); err != nil { + return err + } + } + case Object: + nodes := value.(map[string]*Node) + n.children = make(map[string]*Node, len(nodes)) + for key, node := range nodes { + tkey := key + tnode := node + if err = n.appendNode(&tkey, tnode); err != nil { + return err + } + } + } + n.value.Store(value) + } + return nil +} + +// validate method validates stored value, before update +func (n *Node) validate(_type NodeType, value interface{}) error { + if n == nil { + return errorUnparsed() + } + switch _type { + case Null: + if value != nil { + return errorType() + } + case Numeric: + if _, ok := value.(float64); !ok { + return errorType() + } + case String: + if _, ok := value.(string); !ok { + return errorType() + } + case Bool: + if _, ok := value.(bool); !ok { + return errorType() + } + case Array: + if value != nil { + if _, ok := value.([]*Node); !ok { + return errorType() + } + } + case Object: + if value != nil { + if _, ok := value.(map[string]*Node); !ok { + return errorType() + } + } + } + return nil +} + +// remove method removes value from current container +func (n *Node) remove(value *Node) error { + if !n.isContainer() { + return errorType() + } + if value.parent != n { + return errorRequest("wrong parent") + } + n.mark() + if n.IsArray() { + delete(n.children, strconv.Itoa(*value.index)) + n.dropindex(*value.index) + } else { + delete(n.children, *value.key) + } + value.parent = nil + return nil +} + +// dropindex: internal method to reindexing current array value +func (n *Node) dropindex(index int) { + for i := index + 1; i <= len(n.children); i++ { + previous := i - 1 + if current, ok := n.children[strconv.Itoa(i)]; ok { + current.index = &previous + n.children[strconv.Itoa(previous)] = current + } + delete(n.children, strconv.Itoa(i)) + } +} + +// appendNode appends current Node node value with new Node value, by key or index +func (n *Node) appendNode(key *string, value *Node) error { + if n.isParentOrSelfNode(value) { + return errorRequest("attempt to create infinite loop") + } + if value.parent != nil { + if err := value.parent.remove(value); err != nil { + return err + } + } + value.parent = n + value.key = key + if key != nil { + if old, ok := n.children[*key]; ok { + if old != value { + if err := n.remove(old); err != nil { + return err + } + } + } + n.children[*key] = value + } else { + index := len(n.children) + value.index = &index + n.children[strconv.Itoa(index)] = value + } + return nil +} + +// mark node as dirty, with all parents (up the tree) +func (n *Node) mark() { + node := n + for node != nil && !node.dirty { + node.dirty = true + node = node.parent + } +} + +// clear current value of node +func (n *Node) clear() { + n.data = nil + n.borders[1] = 0 + for key := range n.children { + n.children[key].parent = nil + } + n.children = nil +} + +// isParentOrSelfNode check if current node is the same as given one of parents +func (n *Node) isParentOrSelfNode(node *Node) bool { + return n == node || n.isParentNode(node) +} + +// isParentNode check if current node is one of the parents +func (n *Node) isParentNode(node *Node) bool { + if n != nil { + for current := n.parent; current != nil; current = current.parent { + if current == node { + return true + } + } + } + return false +} + +// setReference updates references of current node +func (n *Node) setReference(parent *Node, key *string, index *int) { + n.parent = parent + if key == nil { + n.key = nil + } else { + temp := *key + n.key = &temp + } + n.index = cptri(index) +} diff --git a/vendor/github.com/spyzhov/ajson/quote.go b/vendor/github.com/spyzhov/ajson/quote.go new file mode 100644 index 0000000..b5b9439 --- /dev/null +++ b/vendor/github.com/spyzhov/ajson/quote.go @@ -0,0 +1,296 @@ +package ajson + +import "unicode/utf8" + +// This file was copied from encoding/json library. +// fixme: https://github.com/spyzhov/ajson/issues/13 +// (c) Golang: encoding/json/tables.go +// (c) Golang: encoding/json/encode.go + +var hex = "0123456789abcdef" + +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} + +// htmlSafeSet holds the value true if the ASCII character with the given +// array position can be safely represented inside a JSON string, embedded +// inside of HTML