├── .github └── workflows │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── callable.go ├── callable_test.go ├── doc.go ├── env.go ├── error.go ├── eval.go ├── eval_test.go ├── example_eval_test.go ├── example_exts_test.go ├── go.mod ├── go.sum ├── jlib ├── aggregate.go ├── array.go ├── boolean.go ├── date.go ├── date_test.go ├── error.go ├── hof.go ├── jlib.go ├── jxpath │ ├── formatdate.go │ ├── formatdate_test.go │ ├── formatnumber.go │ ├── formatnumber_test.go │ └── language.go ├── number.go ├── number_test.go ├── object.go ├── object_test.go ├── string.go └── string_test.go ├── jparse ├── doc.go ├── error.go ├── jparse.go ├── jparse_test.go ├── lexer.go ├── lexer_test.go └── node.go ├── jsonata-server ├── .gitignore ├── README.md ├── bench.go ├── exts.go ├── main.go └── site │ ├── assets │ ├── css │ │ ├── codemirror.min.css │ │ ├── normalize.min.css │ │ └── styles.css │ └── js │ │ ├── codemirror.min.js │ │ ├── javascript.min.js │ │ ├── jsonata-codemirror.js │ │ └── split.min.js │ ├── favicon.ico │ └── index.html ├── jsonata-test ├── .gitignore ├── README.md ├── main.go └── main_test.go ├── jsonata.go ├── jsonata_test.go ├── jtypes ├── funcs.go └── types.go └── testdata ├── account.json ├── account2.json ├── account3.json ├── account4.json ├── account5.json ├── account6.json ├── account7.json ├── address.json ├── foobar.json ├── foobar2.json ├── library.json ├── nest1.json ├── nest2.json └── nest3.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.16.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Install dependencies 18 | run: | 19 | go get -u honnef.co/go/tools/cmd/staticcheck@latest 20 | go get -u golang.org/x/tools/cmd/goimports 21 | - name: Run staticcheck 22 | run: staticcheck ./... 23 | - name: Check code formatting 24 | run: test -z $(goimports -l .) 25 | - name: Run Test 26 | run: go test ./... 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | By participating in this project, you agree to abide by the 4 | [Blues Inc code of conduct][1]. 5 | 6 | [1]: https://blues.github.io/opensource/code-of-conduct 7 | 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to blues/jsonata-go 2 | 3 | We love pull requests from everyone. By participating in this project, you 4 | agree to abide by the Blues Inc [code of conduct]. 5 | 6 | [code of conduct]: https://blues.github.io/opensource/code-of-conduct 7 | 8 | Here are some ways *you* can contribute: 9 | 10 | * by using alpha, beta, and prerelease versions 11 | * by reporting bugs 12 | * by suggesting new features 13 | * by writing or editing documentation 14 | * by writing specifications 15 | * by writing code ( **no patch is too small** : fix typos, add comments, 16 | clean up inconsistent whitespace ) 17 | * by refactoring code 18 | * by closing [issues][] 19 | * by reviewing patches 20 | 21 | [issues]: https://github.com/blues/jsonata-go/issues 22 | 23 | ## Submitting an Issue 24 | 25 | * We use the [GitHub issue tracker][issues] to track bugs and features. 26 | * Before submitting a bug report or feature request, check to make sure it 27 | hasn't 28 | already been submitted. 29 | * When submitting a bug report, please include a [Gist][] that includes a stack 30 | trace and any details that may be necessary to reproduce the bug, including 31 | your release version, stack, and operating system. Ideally, a bug report 32 | should include a pull request with failing specs. 33 | 34 | [gist]: https://gist.github.com/ 35 | 36 | ## Cleaning up issues 37 | 38 | * Issues that have no response from the submitter will be closed after 30 days. 39 | * Issues will be closed once they're assumed to be fixed or answered. If the 40 | maintainer is wrong, it can be opened again. 41 | * If your issue is closed by mistake, please understand and explain the issue. 42 | We will happily reopen the issue. 43 | 44 | ## Submitting a Pull Request 45 | 46 | 1. [Fork][fork] the [official repository][repo]. 47 | 2. [Create a topic branch.][branch] 48 | 3. Implement your feature or bug fix. 49 | 4. Add, commit, and push your changes. 50 | 5. [Submit a pull request.][pr] 51 | 52 | ## Notes 53 | 54 | * Please add tests if you changed code. Contributions without tests won't be accepted. 55 | * If you don't know how to add tests, please put in a PR and leave a comment asking for help. 56 | We love helping! 57 | 58 | [repo]: https://github.com/blues/jsonata-go/tree/master 59 | [fork]: https://help.github.com/articles/fork-a-repo/ 60 | [branch]: 61 | https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/ 62 | [pr]: https://help.github.com/articles/creating-a-pull-request-from-a-fork/ 63 | 64 | Inspired by 65 | https://github.com/thoughtbot/factory_bot/blob/master/CONTRIBUTING.md 66 | 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Blues Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSONata in Go 2 | 3 | Package jsonata is a query and transformation language for JSON. 4 | It's a Go port of the JavaScript library [JSONata](http://jsonata.org/). 5 | 6 | It currently has feature parity with jsonata-js 1.5.4. As well as a most of the functions added in newer versions. You can see potentially missing functions by looking at the [jsonata-js changelog](https://github.com/jsonata-js/jsonata/blob/master/CHANGELOG.md). 7 | 8 | ## Install 9 | 10 | go get github.com/blues/jsonata-go 11 | 12 | ## Usage 13 | 14 | ```Go 15 | import ( 16 | "encoding/json" 17 | "fmt" 18 | "log" 19 | 20 | jsonata "github.com/blues/jsonata-go" 21 | ) 22 | 23 | const jsonString = ` 24 | { 25 | "orders": [ 26 | {"price": 10, "quantity": 3}, 27 | {"price": 0.5, "quantity": 10}, 28 | {"price": 100, "quantity": 1} 29 | ] 30 | } 31 | ` 32 | 33 | func main() { 34 | 35 | var data interface{} 36 | 37 | // Decode JSON. 38 | err := json.Unmarshal([]byte(jsonString), &data) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | // Create expression. 44 | e := jsonata.MustCompile("$sum(orders.(price*quantity))") 45 | 46 | // Evaluate. 47 | res, err := e.Eval(data) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | fmt.Println(res) 53 | // Output: 135 54 | } 55 | ``` 56 | 57 | ## JSONata Server 58 | A locally hosted version of [JSONata Exerciser](http://try.jsonata.org/) 59 | for testing is [available here](https://github.com/blues/jsonata-go/jsonata-server). 60 | 61 | ## JSONata tests 62 | A CLI tool for running jsonata-go against the [JSONata test suite](https://github.com/jsonata-js/jsonata/tree/master/test/test-suite) is [available here](./jsonata-test). 63 | 64 | 65 | 66 | ## Contributing 67 | 68 | We love issues, fixes, and pull requests from everyone. Please run the 69 | unit-tests, staticcheck, and goimports prior to submitting your PR. By participating in this project, you agree to abide by 70 | the Blues Inc [code of conduct](https://blues.github.io/opensource/code-of-conduct). 71 | 72 | For details on contributions we accept and the process for contributing, see our 73 | [contribution guide](CONTRIBUTING.md). 74 | 75 | In addition to the Go unit tests there is also a test runner that will run against the jsonata-js test 76 | suite in the [jsonata-test](./jsonata-test) directory. A number of these tests currently fail, but we're working towards feature parity with the jsonata-js reference implementation. Pull requests welcome! 77 | 78 | If you would like to contribute to this library a good first issue would be to run the jsonata-test suite, 79 | and fix any of the tests not passing. 80 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | // Package jsonata is a query and transformation language for JSON. 6 | // It's a Go port of the JavaScript library JSONata. Please use the 7 | // official JSONata site as a language reference. 8 | // 9 | // http://jsonata.org/ 10 | package jsonata 11 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jsonata 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "regexp" 11 | 12 | "github.com/blues/jsonata-go/jtypes" 13 | ) 14 | 15 | // ErrUndefined is returned by the evaluation methods when 16 | // a JSONata expression yields no results. Unlike most errors, 17 | // ErrUndefined does not mean that evaluation failed. 18 | // 19 | // The simplest way to trigger ErrUndefined is to look up a 20 | // field that is not present in the JSON data. Many JSONata 21 | // operators and functions also return ErrUndefined when 22 | // called with undefined inputs. 23 | var ErrUndefined = errors.New("no results found") 24 | 25 | // ErrType indicates the reason for an error. 26 | type ErrType uint 27 | 28 | // Types of errors that may be encountered by JSONata. 29 | const ( 30 | ErrNonIntegerLHS ErrType = iota 31 | ErrNonIntegerRHS 32 | ErrNonNumberLHS 33 | ErrNonNumberRHS 34 | ErrNonComparableLHS 35 | ErrNonComparableRHS 36 | ErrTypeMismatch 37 | ErrNonCallable 38 | ErrNonCallableApply 39 | ErrNonCallablePartial 40 | ErrNumberInf 41 | ErrNumberNaN 42 | ErrMaxRangeItems 43 | ErrIllegalKey 44 | ErrDuplicateKey 45 | ErrClone 46 | ErrIllegalUpdate 47 | ErrIllegalDelete 48 | ErrNonSortable 49 | ErrSortMismatch 50 | ) 51 | 52 | var errmsgs = map[ErrType]string{ 53 | ErrNonIntegerLHS: `left side of the "{{value}}" operator must evaluate to an integer`, 54 | ErrNonIntegerRHS: `right side of the "{{value}}" operator must evaluate to an integer`, 55 | ErrNonNumberLHS: `left side of the "{{value}}" operator must evaluate to a number`, 56 | ErrNonNumberRHS: `right side of the "{{value}}" operator must evaluate to a number`, 57 | ErrNonComparableLHS: `left side of the "{{value}}" operator must evaluate to a number or string`, 58 | ErrNonComparableRHS: `right side of the "{{value}}" operator must evaluate to a number or string`, 59 | ErrTypeMismatch: `both sides of the "{{value}}" operator must have the same type`, 60 | ErrNonCallable: `cannot call non-function {{token}}`, 61 | ErrNonCallableApply: `cannot use function application with non-function {{token}}`, 62 | ErrNonCallablePartial: `cannot partially apply non-function {{token}}`, 63 | ErrNumberInf: `result of the "{{value}}" operator is out of range`, 64 | ErrNumberNaN: `result of the "{{value}}" operator is not a valid number`, 65 | ErrMaxRangeItems: `range operator has too many items`, 66 | ErrIllegalKey: `object key {{token}} does not evaluate to a string`, 67 | ErrDuplicateKey: `multiple object keys evaluate to the value "{{value}}"`, 68 | ErrClone: `object transformation: cannot make a copy of the object`, 69 | ErrIllegalUpdate: `the insert/update clause of an object transformation must evaluate to an object`, 70 | ErrIllegalDelete: `the delete clause of an object transformation must evaluate to an array of strings`, 71 | ErrNonSortable: `expressions in a sort term must evaluate to strings or numbers`, 72 | ErrSortMismatch: `expressions in a sort term must have the same type`, 73 | } 74 | 75 | var reErrMsg = regexp.MustCompile("{{(token|value)}}") 76 | 77 | // An EvalError represents an error during evaluation of a 78 | // JSONata expression. 79 | type EvalError struct { 80 | Type ErrType 81 | Token string 82 | Value string 83 | } 84 | 85 | func newEvalError(typ ErrType, token interface{}, value interface{}) *EvalError { 86 | 87 | stringify := func(v interface{}) string { 88 | switch v := v.(type) { 89 | case string: 90 | return v 91 | case fmt.Stringer: 92 | return v.String() 93 | default: 94 | return "" 95 | } 96 | } 97 | 98 | return &EvalError{ 99 | Type: typ, 100 | Token: stringify(token), 101 | Value: stringify(value), 102 | } 103 | } 104 | 105 | func (e EvalError) Error() string { 106 | 107 | s := errmsgs[e.Type] 108 | if s == "" { 109 | return fmt.Sprintf("EvalError: unknown error type %d", e.Type) 110 | } 111 | 112 | return reErrMsg.ReplaceAllStringFunc(s, func(match string) string { 113 | switch match { 114 | case "{{token}}": 115 | return e.Token 116 | case "{{value}}": 117 | return e.Value 118 | default: 119 | return match 120 | } 121 | }) 122 | } 123 | 124 | // ArgCountError is returned by the evaluation methods when an 125 | // expression contains a function call with the wrong number of 126 | // arguments. 127 | type ArgCountError struct { 128 | Func string 129 | Expected int 130 | Received int 131 | } 132 | 133 | func newArgCountError(f jtypes.Callable, received int) *ArgCountError { 134 | return &ArgCountError{ 135 | Func: f.Name(), 136 | Expected: f.ParamCount(), 137 | Received: received, 138 | } 139 | } 140 | 141 | func (e ArgCountError) Error() string { 142 | return fmt.Sprintf("function %q takes %d argument(s), got %d", e.Func, e.Expected, e.Received) 143 | } 144 | 145 | // ArgTypeError is returned by the evaluation methods when an 146 | // expression contains a function call with the wrong argument 147 | // type. 148 | type ArgTypeError struct { 149 | Func string 150 | Which int 151 | } 152 | 153 | func newArgTypeError(f jtypes.Callable, which int) *ArgTypeError { 154 | return &ArgTypeError{ 155 | Func: f.Name(), 156 | Which: which, 157 | } 158 | } 159 | 160 | func (e ArgTypeError) Error() string { 161 | return fmt.Sprintf("argument %d of function %q does not match function signature", e.Which, e.Func) 162 | } 163 | -------------------------------------------------------------------------------- /example_eval_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jsonata_test 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "log" 11 | 12 | jsonata "github.com/blues/jsonata-go" 13 | ) 14 | 15 | const jsonString = ` 16 | { 17 | "orders": [ 18 | {"price": 10, "quantity": 3}, 19 | {"price": 0.5, "quantity": 10}, 20 | {"price": 100, "quantity": 1} 21 | ] 22 | } 23 | ` 24 | 25 | func ExampleExpr_Eval() { 26 | 27 | var data interface{} 28 | 29 | // Decode JSON. 30 | err := json.Unmarshal([]byte(jsonString), &data) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | // Create expression. 36 | e := jsonata.MustCompile("$sum(orders.(price*quantity))") 37 | 38 | // Evaluate. 39 | res, err := e.Eval(data) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | fmt.Println(res) 45 | // Output: 135 46 | } 47 | -------------------------------------------------------------------------------- /example_exts_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jsonata_test 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "strings" 11 | 12 | jsonata "github.com/blues/jsonata-go" 13 | ) 14 | 15 | // 16 | // This example demonstrates how to extend JSONata with 17 | // custom functions. 18 | // 19 | 20 | // exts defines a function named "titlecase" which maps to 21 | // the standard library function strings.Title. Any function, 22 | // from the standard library or otherwise, can be used to 23 | // extend JSONata, as long as it returns either one or two 24 | // arguments (the second argument must be an error). 25 | var exts = map[string]jsonata.Extension{ 26 | "titlecase": { 27 | Func: strings.Title, 28 | }, 29 | } 30 | 31 | func ExampleExpr_RegisterExts() { 32 | 33 | // Create an expression that uses the titlecase function. 34 | e := jsonata.MustCompile(`$titlecase("beneath the underdog")`) 35 | 36 | // Register the titlecase function. 37 | err := e.RegisterExts(exts) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | // Evaluate. 43 | res, err := e.Eval(nil) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | fmt.Println(res) 49 | // Output: Beneath The Underdog 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blues/jsonata-go 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blues/jsonata-go/31e6058dd1f53d2ad86f85fc331cadc0631a624b/go.sum -------------------------------------------------------------------------------- /jlib/aggregate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jlib 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | 11 | "github.com/blues/jsonata-go/jtypes" 12 | ) 13 | 14 | // Sum returns the total of an array of numbers. If the array is 15 | // empty, Sum returns 0. 16 | func Sum(v reflect.Value) (float64, error) { 17 | 18 | if !jtypes.IsArray(v) { 19 | if n, ok := jtypes.AsNumber(v); ok { 20 | return n, nil 21 | } 22 | return 0, fmt.Errorf("cannot call sum on a non-array type") 23 | } 24 | 25 | v = jtypes.Resolve(v) 26 | 27 | var sum float64 28 | 29 | for i := 0; i < v.Len(); i++ { 30 | n, ok := jtypes.AsNumber(v.Index(i)) 31 | if !ok { 32 | return 0, fmt.Errorf("cannot call sum on an array with non-number types") 33 | } 34 | sum += n 35 | } 36 | 37 | return sum, nil 38 | } 39 | 40 | // Max returns the largest value in an array of numbers. If the 41 | // array is empty, Max returns 0 and an undefined error. 42 | func Max(v reflect.Value) (float64, error) { 43 | 44 | if !jtypes.IsArray(v) { 45 | if n, ok := jtypes.AsNumber(v); ok { 46 | return n, nil 47 | } 48 | return 0, fmt.Errorf("cannot call max on a non-array type") 49 | } 50 | 51 | v = jtypes.Resolve(v) 52 | if v.Len() == 0 { 53 | return 0, jtypes.ErrUndefined 54 | } 55 | 56 | var max float64 57 | 58 | for i := 0; i < v.Len(); i++ { 59 | n, ok := jtypes.AsNumber(v.Index(i)) 60 | if !ok { 61 | return 0, fmt.Errorf("cannot call max on an array with non-number types") 62 | } 63 | if i == 0 || n > max { 64 | max = n 65 | } 66 | } 67 | 68 | return max, nil 69 | } 70 | 71 | // Min returns the smallest value in an array of numbers. If the 72 | // array is empty, Min returns 0 and an undefined error. 73 | func Min(v reflect.Value) (float64, error) { 74 | 75 | if !jtypes.IsArray(v) { 76 | if n, ok := jtypes.AsNumber(v); ok { 77 | return n, nil 78 | } 79 | return 0, fmt.Errorf("cannot call min on a non-array type") 80 | } 81 | 82 | v = jtypes.Resolve(v) 83 | if v.Len() == 0 { 84 | return 0, jtypes.ErrUndefined 85 | } 86 | 87 | var min float64 88 | 89 | for i := 0; i < v.Len(); i++ { 90 | n, ok := jtypes.AsNumber(v.Index(i)) 91 | if !ok { 92 | return 0, fmt.Errorf("cannot call min on an array with non-number types") 93 | } 94 | if i == 0 || n < min { 95 | min = n 96 | } 97 | } 98 | 99 | return min, nil 100 | } 101 | 102 | // Average returns the mean of an array of numbers. If the array 103 | // is empty, Average returns 0 and an undefined error. 104 | func Average(v reflect.Value) (float64, error) { 105 | 106 | if !jtypes.IsArray(v) { 107 | if n, ok := jtypes.AsNumber(v); ok { 108 | return n, nil 109 | } 110 | return 0, fmt.Errorf("cannot call average on a non-array type") 111 | } 112 | 113 | v = jtypes.Resolve(v) 114 | if v.Len() == 0 { 115 | return 0, jtypes.ErrUndefined 116 | } 117 | 118 | var sum float64 119 | 120 | for i := 0; i < v.Len(); i++ { 121 | n, ok := jtypes.AsNumber(v.Index(i)) 122 | if !ok { 123 | return 0, fmt.Errorf("cannot call average on an array with non-number types") 124 | } 125 | sum += n 126 | } 127 | 128 | return sum / float64(v.Len()), nil 129 | } 130 | -------------------------------------------------------------------------------- /jlib/array.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jlib 6 | 7 | import ( 8 | "fmt" 9 | "math/rand" 10 | "reflect" 11 | "sort" 12 | 13 | "github.com/blues/jsonata-go/jtypes" 14 | ) 15 | 16 | // Count (golint) 17 | func Count(v reflect.Value) int { 18 | v = jtypes.Resolve(v) 19 | 20 | if !jtypes.IsArray(v) { 21 | if v.IsValid() { 22 | return 1 23 | } 24 | return 0 25 | } 26 | 27 | return v.Len() 28 | } 29 | 30 | // Distinct returns the values passed in with any duplicates removed. 31 | func Distinct(v reflect.Value) interface{} { 32 | v = jtypes.Resolve(v) 33 | 34 | // To match the behavior of jsonata-js, if this is a string we should 35 | // return the entire string and not dedupe the individual characters 36 | if jtypes.IsString(v) { 37 | return v.String() 38 | } 39 | 40 | if jtypes.IsArray(v) { 41 | items := arrayify(v) 42 | visited := make(map[interface{}]struct{}) 43 | distinctValues := reflect.MakeSlice(reflect.SliceOf(typeInterface), 0, 0) 44 | 45 | for i := 0; i < items.Len(); i++ { 46 | item := jtypes.Resolve(items.Index(i)) 47 | 48 | if jtypes.IsMap(item) { 49 | // We can't hash a map, so convert it to a 50 | // string that is hashable 51 | mapItem := fmt.Sprint(item.Interface()) 52 | if _, ok := visited[mapItem]; ok { 53 | continue 54 | } 55 | visited[mapItem] = struct{}{} 56 | distinctValues = reflect.Append(distinctValues, item) 57 | 58 | continue 59 | } 60 | 61 | if _, ok := visited[item.Interface()]; ok { 62 | continue 63 | } 64 | 65 | visited[item.Interface()] = struct{}{} 66 | distinctValues = reflect.Append(distinctValues, item) 67 | } 68 | return distinctValues.Interface() 69 | } 70 | 71 | return nil 72 | } 73 | 74 | // Append (golint) 75 | func Append(v1, v2 reflect.Value) (interface{}, error) { 76 | if !v2.IsValid() && v1.IsValid() && v1.CanInterface() { 77 | return v1.Interface(), nil 78 | } 79 | 80 | if !v1.IsValid() && v2.IsValid() && v2.CanInterface() { 81 | return v2.Interface(), nil 82 | } 83 | 84 | v1 = arrayify(v1) 85 | v2 = arrayify(v2) 86 | 87 | len1 := v1.Len() 88 | len2 := v2.Len() 89 | 90 | results := reflect.MakeSlice(reflect.SliceOf(typeInterface), 0, len1+len2) 91 | 92 | appendSlice := func(vs reflect.Value, length int) { 93 | for i := 0; i < length; i++ { 94 | if item := vs.Index(i); item.IsValid() { 95 | results = reflect.Append(results, item) 96 | } 97 | } 98 | } 99 | 100 | appendSlice(v1, len1) 101 | appendSlice(v2, len2) 102 | 103 | return results.Interface(), nil 104 | } 105 | 106 | // Reverse (golint) 107 | func Reverse(v reflect.Value) (interface{}, error) { 108 | v = arrayify(v) 109 | length := v.Len() 110 | 111 | results := reflect.MakeSlice(v.Type(), 0, length) 112 | 113 | for i := length - 1; i >= 0; i-- { 114 | if item := v.Index(i); item.IsValid() { 115 | results = reflect.Append(results, item) 116 | } 117 | } 118 | 119 | return results.Interface(), nil 120 | } 121 | 122 | // Sort (golint) 123 | func Sort(v reflect.Value, swap jtypes.OptionalCallable) (interface{}, error) { 124 | v = jtypes.Resolve(v) 125 | 126 | switch { 127 | case !v.IsValid(): 128 | return nil, jtypes.ErrUndefined 129 | case !jtypes.IsArray(v): 130 | if v.CanInterface() { 131 | return []interface{}{v.Interface()}, nil 132 | } 133 | case swap.Callable != nil: 134 | return sortArrayFunc(v, swap.Callable) 135 | case jtypes.IsArrayOf(v, jtypes.IsNumber): 136 | return sortNumberArray(v), nil 137 | case jtypes.IsArrayOf(v, jtypes.IsString): 138 | return sortStringArray(v), nil 139 | } 140 | 141 | return nil, fmt.Errorf("argument 1 of function sort must be an array of strings or numbers") 142 | } 143 | 144 | func sortNumberArray(v reflect.Value) []interface{} { 145 | size := v.Len() 146 | results := make([]interface{}, 0, size) 147 | 148 | for i := 0; i < size; i++ { 149 | if n, ok := jtypes.AsNumber(v.Index(i)); ok { 150 | results = append(results, n) 151 | } 152 | } 153 | 154 | sort.SliceStable(results, func(i, j int) bool { 155 | return results[i].(float64) < results[j].(float64) 156 | }) 157 | 158 | return results 159 | } 160 | 161 | func sortStringArray(v reflect.Value) []interface{} { 162 | size := v.Len() 163 | results := make([]interface{}, 0, size) 164 | 165 | for i := 0; i < size; i++ { 166 | if s, ok := jtypes.AsString(v.Index(i)); ok { 167 | results = append(results, s) 168 | } 169 | } 170 | 171 | sort.SliceStable(results, func(i, j int) bool { 172 | return results[i].(string) < results[j].(string) 173 | }) 174 | 175 | return results 176 | } 177 | 178 | func sortArrayFunc(v reflect.Value, fn jtypes.Callable) (interface{}, error) { 179 | size := v.Len() 180 | results := make([]interface{}, 0, size) 181 | 182 | for i := 0; i < size; i++ { 183 | if item := v.Index(i); item.CanInterface() { 184 | results = append(results, item.Interface()) 185 | } 186 | } 187 | 188 | swapFunc := func(lhs, rhs interface{}) (bool, error) { 189 | 190 | args := []reflect.Value{ 191 | reflect.ValueOf(lhs), 192 | reflect.ValueOf(rhs), 193 | } 194 | 195 | v, err := fn.Call(args) 196 | if err != nil { 197 | return false, err 198 | } 199 | 200 | b, ok := jtypes.AsBool(v) 201 | if !ok { 202 | return false, fmt.Errorf("argument 2 of function sort must be a function that returns a boolean, got %v (%s)", v, v.Kind()) 203 | } 204 | 205 | return b, nil 206 | } 207 | 208 | return mergeSort(results, swapFunc) 209 | } 210 | 211 | func mergeSort(values []interface{}, swapFunc func(interface{}, interface{}) (bool, error)) ([]interface{}, error) { 212 | n := len(values) 213 | if n < 2 { 214 | return values, nil 215 | } 216 | 217 | pos := n / 2 218 | lhs, err := mergeSort(values[:pos], swapFunc) 219 | if err != nil { 220 | return nil, err 221 | } 222 | rhs, err := mergeSort(values[pos:], swapFunc) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | return merge(lhs, rhs, swapFunc) 228 | } 229 | 230 | func merge(lhs, rhs []interface{}, swapFunc func(interface{}, interface{}) (bool, error)) ([]interface{}, error) { 231 | results := make([]interface{}, len(lhs)+len(rhs)) 232 | 233 | for i := range results { 234 | 235 | if len(rhs) == 0 { 236 | results = append(results[:i], lhs...) 237 | break 238 | } 239 | 240 | if len(lhs) == 0 { 241 | results = append(results[:i], rhs...) 242 | break 243 | } 244 | 245 | swap, err := swapFunc(lhs[0], rhs[0]) 246 | if err != nil { 247 | return nil, err 248 | } 249 | 250 | if swap { 251 | results[i] = rhs[0] 252 | rhs = rhs[1:] 253 | } else { 254 | results[i] = lhs[0] 255 | lhs = lhs[1:] 256 | } 257 | } 258 | 259 | return results, nil 260 | } 261 | 262 | // Shuffle (golint) 263 | func Shuffle(v reflect.Value) interface{} { 264 | v = forceArray(jtypes.Resolve(v)) 265 | 266 | length := arrayLen(v) 267 | results := make([]interface{}, length) 268 | 269 | for i := 0; i < length; i++ { 270 | 271 | j := rand.Intn(i + 1) 272 | 273 | if i != j { 274 | results[i] = results[j] 275 | } 276 | 277 | item := v.Index(i) 278 | if item.IsValid() && item.CanInterface() { 279 | results[j] = item.Interface() 280 | } 281 | } 282 | 283 | return results 284 | } 285 | 286 | // Zip (golint) 287 | func Zip(vs ...reflect.Value) (interface{}, error) { 288 | var size int 289 | 290 | if len(vs) == 0 { 291 | return nil, fmt.Errorf("cannot call zip with no arguments") 292 | } 293 | 294 | for i := 0; i < len(vs); i++ { 295 | 296 | vs[i] = forceArray(jtypes.Resolve(vs[i])) 297 | if !vs[i].IsValid() { 298 | return []interface{}{}, nil 299 | } 300 | 301 | if i == 0 || arrayLen(vs[i]) < size { 302 | size = arrayLen(vs[i]) 303 | } 304 | } 305 | 306 | result := make([]interface{}, size) 307 | 308 | for i := 0; i < size; i++ { 309 | 310 | inner := make([]interface{}, len(vs)) 311 | 312 | for j := 0; j < len(vs); j++ { 313 | v := vs[j].Index(i) 314 | if v.IsValid() && v.CanInterface() { 315 | inner[j] = v.Interface() 316 | } 317 | } 318 | 319 | result[i] = inner 320 | } 321 | 322 | return result, nil 323 | } 324 | 325 | func forceArray(v reflect.Value) reflect.Value { 326 | v = jtypes.Resolve(v) 327 | if !v.IsValid() || jtypes.IsArray(v) { 328 | return v 329 | } 330 | vs := reflect.MakeSlice(reflect.SliceOf(v.Type()), 0, 1) 331 | vs = reflect.Append(vs, v) 332 | return vs 333 | } 334 | 335 | func arrayLen(v reflect.Value) int { 336 | if jtypes.IsArray(v) { 337 | return v.Len() 338 | } 339 | return 0 340 | } 341 | 342 | var typeInterface = reflect.TypeOf((*interface{})(nil)).Elem() 343 | 344 | func arrayify(v reflect.Value) reflect.Value { 345 | switch { 346 | case jtypes.IsArray(v): 347 | return jtypes.Resolve(v) 348 | case !v.IsValid(): 349 | return reflect.MakeSlice(reflect.SliceOf(typeInterface), 0, 0) 350 | default: 351 | return reflect.Append(reflect.MakeSlice(reflect.SliceOf(typeInterface), 0, 1), v) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /jlib/boolean.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jlib 6 | 7 | import ( 8 | "reflect" 9 | 10 | "github.com/blues/jsonata-go/jtypes" 11 | ) 12 | 13 | // Boolean (golint) 14 | func Boolean(v reflect.Value) bool { 15 | 16 | v = jtypes.Resolve(v) 17 | 18 | if b, ok := jtypes.AsBool(v); ok { 19 | return b 20 | } 21 | 22 | if s, ok := jtypes.AsString(v); ok { 23 | return s != "" 24 | } 25 | 26 | if n, ok := jtypes.AsNumber(v); ok { 27 | return n != 0 28 | } 29 | 30 | if jtypes.IsArray(v) { 31 | for i := 0; i < v.Len(); i++ { 32 | if Boolean(v.Index(i)) { 33 | return true 34 | } 35 | } 36 | return false 37 | } 38 | 39 | if jtypes.IsMap(v) { 40 | return v.Len() > 0 41 | } 42 | 43 | return false 44 | } 45 | 46 | // Not (golint) 47 | func Not(v reflect.Value) bool { 48 | return !Boolean(v) 49 | } 50 | 51 | // Exists (golint) 52 | func Exists(v reflect.Value) bool { 53 | return v.IsValid() 54 | } 55 | -------------------------------------------------------------------------------- /jlib/date.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jlib 6 | 7 | import ( 8 | "fmt" 9 | "regexp" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/blues/jsonata-go/jlib/jxpath" 14 | "github.com/blues/jsonata-go/jtypes" 15 | ) 16 | 17 | // 2006-01-02T15:04:05.000Z07:00 18 | const defaultFormatTimeLayout = "[Y]-[M01]-[D01]T[H01]:[m]:[s].[f001][Z01:01t]" 19 | 20 | var defaultParseTimeLayouts = []string{ 21 | "[Y]-[M01]-[D01]T[H01]:[m]:[s][Z01:01t]", 22 | "[Y]-[M01]-[D01]T[H01]:[m]:[s][Z0100t]", 23 | "[Y]-[M01]-[D01]T[H01]:[m]:[s]", 24 | "[Y]-[M01]-[D01]", 25 | "[Y]", 26 | } 27 | 28 | // FromMillis (golint) 29 | func FromMillis(ms int64, picture jtypes.OptionalString, tz jtypes.OptionalString) (string, error) { 30 | 31 | t := msToTime(ms).UTC() 32 | 33 | if tz.String != "" { 34 | loc, err := parseTimeZone(tz.String) 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | t = t.In(loc) 40 | } 41 | 42 | layout := picture.String 43 | if layout == "" { 44 | layout = defaultFormatTimeLayout 45 | } 46 | 47 | return jxpath.FormatTime(t, layout) 48 | } 49 | 50 | // parseTimeZone parses a JSONata timezone. 51 | // 52 | // The format is a "+" or "-" character, followed by four digits, the first two 53 | // denoting the hour offset, and the last two denoting the minute offset. 54 | func parseTimeZone(tz string) (*time.Location, error) { 55 | // must be exactly 5 characters 56 | if len(tz) != 5 { 57 | return nil, fmt.Errorf("invalid timezone") 58 | } 59 | 60 | plusOrMinus := string(tz[0]) 61 | 62 | // the first character must be a literal "+" or "-" character. 63 | // Any other character will error. 64 | var offsetMultiplier int 65 | switch plusOrMinus { 66 | case "-": 67 | offsetMultiplier = -1 68 | case "+": 69 | offsetMultiplier = 1 70 | default: 71 | return nil, fmt.Errorf("invalid timezone") 72 | } 73 | 74 | // take the first two digits as "HH" 75 | hours, err := strconv.Atoi(tz[1:3]) 76 | if err != nil { 77 | return nil, fmt.Errorf("invalid timezone") 78 | } 79 | 80 | // take the last two digits as "MM" 81 | minutes, err := strconv.Atoi(tz[3:5]) 82 | if err != nil { 83 | return nil, fmt.Errorf("invalid timezone") 84 | } 85 | 86 | // convert to seconds 87 | offsetSeconds := offsetMultiplier * (60 * ((60 * hours) + minutes)) 88 | 89 | // construct a time.Location based on the tz string and the offset in seconds. 90 | loc := time.FixedZone(tz, offsetSeconds) 91 | 92 | return loc, nil 93 | } 94 | 95 | // ToMillis (golint) 96 | func ToMillis(s string, picture jtypes.OptionalString, tz jtypes.OptionalString) (int64, error) { 97 | layouts := defaultParseTimeLayouts 98 | if picture.String != "" { 99 | layouts = []string{picture.String} 100 | } 101 | 102 | // TODO: How are timezones used for parsing? 103 | 104 | for _, l := range layouts { 105 | if t, err := parseTime(s, l); err == nil { 106 | return timeToMS(t), nil 107 | } 108 | } 109 | 110 | return 0, fmt.Errorf("could not parse time %q", s) 111 | } 112 | 113 | var reMinus7 = regexp.MustCompile("-(0*7)") 114 | 115 | func parseTime(s string, picture string) (time.Time, error) { 116 | // Go's reference time: Mon Jan 2 15:04:05 MST 2006 117 | refTime := time.Date(2006, time.January, 2, 15, 4, 5, 0, time.FixedZone("MST", -7*60*60)) 118 | 119 | layout, err := jxpath.FormatTime(refTime, picture) 120 | if err != nil { 121 | return time.Time{}, fmt.Errorf("the second argument of the toMillis function must be a valid date format") 122 | } 123 | 124 | // Replace -07:00 with Z07:00 125 | layout = reMinus7.ReplaceAllString(layout, "Z$1") 126 | 127 | t, err := time.Parse(layout, s) 128 | if err != nil { 129 | return time.Time{}, fmt.Errorf("could not parse time %q", s) 130 | } 131 | 132 | return t, nil 133 | } 134 | 135 | func msToTime(ms int64) time.Time { 136 | return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)) 137 | } 138 | 139 | func timeToMS(t time.Time) int64 { 140 | return t.UnixNano() / int64(time.Millisecond) 141 | } 142 | -------------------------------------------------------------------------------- /jlib/date_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jlib_test 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | "time" 11 | 12 | "github.com/blues/jsonata-go/jlib" 13 | "github.com/blues/jsonata-go/jtypes" 14 | ) 15 | 16 | func TestFromMillis(t *testing.T) { 17 | 18 | date := time.Date(2018, time.September, 30, 15, 58, 5, int(762*time.Millisecond), time.UTC) 19 | input := date.UnixNano() / int64(time.Millisecond) 20 | 21 | data := []struct { 22 | Picture string 23 | TZ string 24 | Output string 25 | ExpectedError bool 26 | }{ 27 | { 28 | Picture: "[Y0001]-[M01]-[D01]", 29 | Output: "2018-09-30", 30 | }, 31 | /*{ 32 | Picture: "[[[Y0001]-[M01]-[D01]]]", 33 | Output: "[2018-09-30]", 34 | },*/ 35 | { 36 | Picture: "[M]-[D]-[Y]", 37 | Output: "9-30-2018", 38 | }, 39 | { 40 | Picture: "[D1o] [MNn], [Y]", 41 | Output: "30th September, 2018", 42 | }, 43 | { 44 | Picture: "[D01] [MN,*-3] [Y0001]", 45 | Output: "30 SEP 2018", 46 | }, 47 | { 48 | Picture: "[h]:[m01] [PN]", 49 | Output: "3:58 PM", 50 | }, 51 | { 52 | Picture: "[h]:[m01]:[s01] [Pn]", 53 | Output: "3:58:05 pm", 54 | }, 55 | { 56 | Picture: "[h]:[m01]:[s01] [PN] [ZN,*-3]", 57 | Output: "3:58:05 PM UTC", 58 | }, 59 | { 60 | Picture: "[h]:[m01]:[s01] o'clock [PN] [ZN,*-3]", 61 | Output: "3:58:05 o'clock PM UTC", 62 | }, 63 | { 64 | Picture: "[H01]:[m01]:[s01].[f001]", 65 | Output: "15:58:05.762", 66 | }, 67 | { 68 | Picture: "[H01]:[m01]:[s01] [Z]", 69 | TZ: "+0200", 70 | Output: "17:58:05 +02:00", 71 | }, 72 | { 73 | Picture: "[H01]:[m01]:[s01] [z]", 74 | TZ: "-0500", 75 | Output: "10:58:05 GMT-05:00", 76 | }, 77 | { 78 | Picture: "[H01]:[m01]:[s01] [z]", 79 | TZ: "-0630", 80 | Output: "09:28:05 GMT-06:30", 81 | }, 82 | { 83 | Picture: "[H01]:[m01]:[s01] [z]", 84 | // Invalid TZ 85 | TZ: "-0", 86 | ExpectedError: true, 87 | }, 88 | { 89 | Picture: "[h].[m01][Pn] on [FNn], [D1o] [MNn]", 90 | Output: "3.58pm on Sunday, 30th September", 91 | }, 92 | { 93 | Picture: "[M01]/[D01]/[Y0001] at [H01]:[m01]:[s01]", 94 | Output: "09/30/2018 at 15:58:05", 95 | }, 96 | } 97 | 98 | for _, test := range data { 99 | 100 | var picture jtypes.OptionalString 101 | var tz jtypes.OptionalString 102 | 103 | if test.Picture != "" { 104 | picture.Set(reflect.ValueOf(test.Picture)) 105 | } 106 | 107 | if test.TZ != "" { 108 | tz.Set(reflect.ValueOf(test.TZ)) 109 | } 110 | 111 | got, err := jlib.FromMillis(input, picture, tz) 112 | 113 | if test.ExpectedError && err == nil { 114 | t.Errorf("%s: Expected error, got nil", test.Picture) 115 | } else if got != test.Output { 116 | t.Errorf("%s: Expected %q, got %q", test.Picture, test.Output, got) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /jlib/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jlib 6 | 7 | import "fmt" 8 | 9 | // ErrType (golint) 10 | type ErrType uint 11 | 12 | // ErrNanInf (golint) 13 | const ( 14 | _ ErrType = iota 15 | ErrNaNInf 16 | ) 17 | 18 | // Error (golint) 19 | type Error struct { 20 | Type ErrType 21 | Func string 22 | } 23 | 24 | // Error (golint) 25 | func (e Error) Error() string { 26 | 27 | var msg string 28 | 29 | switch e.Type { 30 | case ErrNaNInf: 31 | msg = "cannot convert NaN/Infinity to string" 32 | default: 33 | msg = "unknown error" 34 | } 35 | 36 | return fmt.Sprintf("%s: %s", e.Func, msg) 37 | } 38 | 39 | func newError(name string, typ ErrType) *Error { 40 | return &Error{ 41 | Func: name, 42 | Type: typ, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /jlib/hof.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jlib 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | 11 | "github.com/blues/jsonata-go/jtypes" 12 | ) 13 | 14 | // Map (golint) 15 | func Map(v reflect.Value, f jtypes.Callable) (interface{}, error) { 16 | 17 | v = forceArray(jtypes.Resolve(v)) 18 | 19 | var results []interface{} 20 | 21 | argc := clamp(f.ParamCount(), 1, 3) 22 | 23 | for i := 0; i < arrayLen(v); i++ { 24 | 25 | argv := []reflect.Value{v.Index(i), reflect.ValueOf(i), v} 26 | 27 | res, err := f.Call(argv[:argc]) 28 | if err != nil { 29 | return nil, err 30 | } 31 | if res.IsValid() && res.CanInterface() { 32 | results = append(results, res.Interface()) 33 | } 34 | } 35 | 36 | return results, nil 37 | } 38 | 39 | // Filter (golint) 40 | func Filter(v reflect.Value, f jtypes.Callable) (interface{}, error) { 41 | 42 | v = forceArray(jtypes.Resolve(v)) 43 | 44 | var results []interface{} 45 | 46 | argc := clamp(f.ParamCount(), 1, 3) 47 | 48 | for i := 0; i < arrayLen(v); i++ { 49 | 50 | item := v.Index(i) 51 | argv := []reflect.Value{item, reflect.ValueOf(i), v} 52 | 53 | res, err := f.Call(argv[:argc]) 54 | if err != nil { 55 | return nil, err 56 | } 57 | if Boolean(res) && item.IsValid() && item.CanInterface() { 58 | results = append(results, item.Interface()) 59 | } 60 | } 61 | 62 | return results, nil 63 | } 64 | 65 | // Reduce (golint) 66 | func Reduce(v reflect.Value, f jtypes.Callable, init jtypes.OptionalValue) (interface{}, error) { 67 | 68 | v = forceArray(jtypes.Resolve(v)) 69 | 70 | var res reflect.Value 71 | 72 | if f.ParamCount() != 2 { 73 | return nil, fmt.Errorf("second argument of function \"reduce\" must be a function that takes two arguments") 74 | } 75 | 76 | i := 0 77 | switch { 78 | case init.IsSet(): 79 | res = jtypes.Resolve(init.Value) 80 | case arrayLen(v) > 0: 81 | res = v.Index(0) 82 | i = 1 83 | } 84 | 85 | var err error 86 | for ; i < arrayLen(v); i++ { 87 | res, err = f.Call([]reflect.Value{res, v.Index(i)}) 88 | if err != nil { 89 | return nil, err 90 | } 91 | } 92 | 93 | if !res.IsValid() || !res.CanInterface() { 94 | return nil, jtypes.ErrUndefined 95 | } 96 | 97 | return res.Interface(), nil 98 | } 99 | 100 | // Single returns the one and only one value in the array parameter that satisfy 101 | // the function predicate (i.e. function returns Boolean true when passed the 102 | // value). Returns an error if the number of matching values is not exactly 103 | // one. 104 | // https://docs.jsonata.org/higher-order-functions#single 105 | func Single(v reflect.Value, f jtypes.Callable) (interface{}, error) { 106 | filteredValue, err := Filter(v, f) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | switch reflect.TypeOf(filteredValue).Kind() { 112 | case reflect.Slice: 113 | // Since Filter() returned a slice, if there is either zero or 114 | // more than one item in the slice, return a error, otherwise 115 | // return the item 116 | s := reflect.ValueOf(filteredValue) 117 | if s.Len() != 1 { 118 | return nil, fmt.Errorf("number of matching values returned by single() must be 1, got: %d", s.Len()) 119 | } 120 | return s.Index(0).Interface(), nil 121 | 122 | default: 123 | // Filter returned a single value, so use that 124 | return reflect.ValueOf(filteredValue).Interface(), nil 125 | } 126 | } 127 | 128 | func clamp(n, min, max int) int { 129 | switch { 130 | case n < min: 131 | return min 132 | case n > max: 133 | return max 134 | default: 135 | return n 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /jlib/jlib.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | // Package jlib implements the JSONata function library. 6 | package jlib 7 | 8 | import ( 9 | "fmt" 10 | "math/rand" 11 | "reflect" 12 | "time" 13 | 14 | "github.com/blues/jsonata-go/jtypes" 15 | ) 16 | 17 | func init() { 18 | // Seed random numbers for Random() and Shuffle(). 19 | rand.Seed(time.Now().UnixNano()) 20 | } 21 | 22 | var typeBool = reflect.TypeOf((*bool)(nil)).Elem() 23 | var typeCallable = reflect.TypeOf((*jtypes.Callable)(nil)).Elem() 24 | var typeString = reflect.TypeOf((*string)(nil)).Elem() 25 | var typeNumber = reflect.TypeOf((*float64)(nil)).Elem() 26 | 27 | // StringNumberBool (golint) 28 | type StringNumberBool reflect.Value 29 | 30 | // ValidTypes (golint) 31 | func (StringNumberBool) ValidTypes() []reflect.Type { 32 | return []reflect.Type{ 33 | typeBool, 34 | typeString, 35 | typeNumber, 36 | } 37 | } 38 | 39 | // StringCallable (golint) 40 | type StringCallable reflect.Value 41 | 42 | // ValidTypes (golint) 43 | func (StringCallable) ValidTypes() []reflect.Type { 44 | return []reflect.Type{ 45 | typeString, 46 | typeCallable, 47 | } 48 | } 49 | 50 | func (s StringCallable) toInterface() interface{} { 51 | if v := reflect.Value(s); v.IsValid() && v.CanInterface() { 52 | return v.Interface() 53 | } 54 | return nil 55 | } 56 | 57 | // TypeOf implements the jsonata $type function that returns the data type of 58 | // the argument 59 | func TypeOf(x interface{}) (string, error) { 60 | v := reflect.ValueOf(x) 61 | if jtypes.IsCallable(v) { 62 | return "function", nil 63 | } 64 | if jtypes.IsString(v) { 65 | return "string", nil 66 | } 67 | if jtypes.IsNumber(v) { 68 | return "number", nil 69 | } 70 | if jtypes.IsArray(v) { 71 | return "array", nil 72 | } 73 | if jtypes.IsBool(v) { 74 | return "boolean", nil 75 | } 76 | if jtypes.IsMap(v) { 77 | return "object", nil 78 | } 79 | 80 | switch x.(type) { 81 | case *interface{}: 82 | return "null", nil 83 | } 84 | 85 | xType := reflect.TypeOf(x).String() 86 | return "", fmt.Errorf("unknown type %s", xType) 87 | } 88 | -------------------------------------------------------------------------------- /jlib/jxpath/formatdate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jxpath 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestFormatYear(t *testing.T) { 14 | 15 | input := time.Date(2018, time.April, 1, 12, 0, 0, 0, time.UTC) 16 | 17 | data := []struct { 18 | Picture string 19 | Output string 20 | Error error 21 | }{ 22 | { 23 | // Default layout is 1. 24 | Picture: "[Y]", 25 | Output: "2018", 26 | }, 27 | { 28 | Picture: "[Y1]", 29 | Output: "2018", 30 | }, 31 | { 32 | Picture: "[Y01]", 33 | Output: "18", 34 | }, 35 | { 36 | Picture: "[Y001]", 37 | Output: "018", 38 | }, 39 | { 40 | Picture: "[Y0001]", 41 | Output: "2018", 42 | }, 43 | { 44 | Picture: "[Y9,999,*]", 45 | Output: "2,018", 46 | }, 47 | { 48 | Picture: "[Y1,*-1]", 49 | Output: "8", 50 | }, 51 | { 52 | Picture: "[Y1,*-2]", 53 | Output: "18", 54 | }, 55 | /*{ 56 | Picture: "[Y1,*-3]", 57 | Output: "018", 58 | },*/ 59 | { 60 | Picture: "[Y1,*-4]", 61 | Output: "2018", 62 | }, 63 | { 64 | // Unsupported layouts should fall back to the default. 65 | Picture: "[YNn]", 66 | Output: "2018", 67 | }, 68 | } 69 | 70 | for _, test := range data { 71 | 72 | got, err := FormatTime(input, test.Picture) 73 | 74 | if got != test.Output { 75 | t.Errorf("%s: Expected %q, got %q", test.Picture, test.Output, got) 76 | } 77 | 78 | if !reflect.DeepEqual(err, test.Error) { 79 | t.Errorf("%s: Expected error %v, got %v", test.Picture, test.Error, err) 80 | } 81 | } 82 | } 83 | 84 | func TestFormatTimezone(t *testing.T) { 85 | 86 | const minutes = 60 87 | const hours = 60 * minutes 88 | 89 | timezones := []struct { 90 | Name string 91 | Offset int 92 | }{ 93 | { 94 | Name: "HST", 95 | Offset: -10 * hours, 96 | }, 97 | { 98 | Name: "EST", 99 | Offset: -5 * hours, 100 | }, 101 | { 102 | Name: "GMT", 103 | Offset: 0, 104 | }, 105 | { 106 | Name: "IST", 107 | Offset: 5*hours + 30*minutes, 108 | }, 109 | { 110 | Offset: 13 * hours, 111 | }, 112 | } 113 | 114 | times := make([]time.Time, len(timezones)) 115 | for i, tz := range timezones { 116 | // We're mostly interested in the timezone for these tests 117 | // so the exact date used here doesn't matter. But the time 118 | // must be 12:00 for the final test case (which also outputs 119 | // the time) to work. 120 | times[i] = time.Date(2018, time.April, 1, 12, 0, 0, 0, time.FixedZone(tz.Name, tz.Offset)) 121 | } 122 | 123 | data := []struct { 124 | Picture string 125 | Location *time.Location 126 | Outputs []string 127 | }{ 128 | { 129 | // Default layout is 00:00. 130 | Picture: "[Z]", 131 | Outputs: []string{ 132 | "-10:00", 133 | "-05:00", 134 | "+00:00", 135 | "+05:30", 136 | "+13:00", 137 | }, 138 | }, 139 | { 140 | Picture: "[Z0]", 141 | Outputs: []string{ 142 | "-10", 143 | "-5", 144 | "+0", 145 | "+5:30", 146 | "+13", 147 | }, 148 | }, 149 | { 150 | Picture: "[Z00]", 151 | Outputs: []string{ 152 | "-10", 153 | "-05", 154 | "+00", 155 | "+05:30", 156 | "+13", 157 | }, 158 | }, 159 | { 160 | Picture: "[Z00t]", 161 | Outputs: []string{ 162 | "-10", 163 | "-05", 164 | "Z", 165 | "+05:30", 166 | "+13", 167 | }, 168 | }, 169 | { 170 | Picture: "[Z000]", 171 | Outputs: []string{ 172 | "-1000", 173 | "-500", 174 | "+000", 175 | "+530", 176 | "+1300", 177 | }, 178 | }, 179 | { 180 | Picture: "[Z0000]", 181 | Outputs: []string{ 182 | "-1000", 183 | "-0500", 184 | "+0000", 185 | "+0530", 186 | "+1300", 187 | }, 188 | }, 189 | { 190 | Picture: "[Z0000t]", 191 | Outputs: []string{ 192 | "-1000", 193 | "-0500", 194 | "Z", 195 | "+0530", 196 | "+1300", 197 | }, 198 | }, 199 | { 200 | Picture: "[Z0:00]", 201 | Outputs: []string{ 202 | "-10:00", 203 | "-5:00", 204 | "+0:00", 205 | "+5:30", 206 | "+13:00", 207 | }, 208 | }, 209 | { 210 | Picture: "[Z00:00]", 211 | Outputs: []string{ 212 | "-10:00", 213 | "-05:00", 214 | "+00:00", 215 | "+05:30", 216 | "+13:00", 217 | }, 218 | }, 219 | { 220 | Picture: "[Z00:00t]", 221 | Outputs: []string{ 222 | "-10:00", 223 | "-05:00", 224 | "Z", 225 | "+05:30", 226 | "+13:00", 227 | }, 228 | }, 229 | { 230 | Picture: "[z]", 231 | Outputs: []string{ 232 | "GMT-10:00", 233 | "GMT-05:00", 234 | "GMT+00:00", 235 | "GMT+05:30", 236 | "GMT+13:00", 237 | }, 238 | }, 239 | { 240 | Picture: "[ZZ]", 241 | Outputs: []string{ 242 | "W", 243 | "R", 244 | "Z", 245 | "+05:30", // military layout only supports whole hour offsets, fall back to the default 246 | "+13:00", // military layout only supports offsets up to 12 hours, fall back to the default 247 | }, 248 | }, 249 | { 250 | Picture: "[ZN]", 251 | Outputs: []string{ 252 | "HST", 253 | "EST", 254 | "GMT", 255 | "IST", 256 | "+13:00", // timezone has no name, fall back to the default layout 257 | }, 258 | }, 259 | { 260 | Picture: "[H00]:[m00] [ZN]", 261 | Location: time.FixedZone("EST", -5*hours), 262 | Outputs: []string{ 263 | "17:00 EST", // Note: The XPath docs (incorrectly) have this as 06:00 EST 264 | "12:00 EST", 265 | "07:00 EST", 266 | "01:30 EST", 267 | "18:00 EST", 268 | }, 269 | }, 270 | } 271 | 272 | for _, test := range data { 273 | for i, tm := range times { 274 | 275 | if test.Location != nil { 276 | tm = tm.In(test.Location) 277 | } 278 | 279 | got, err := FormatTime(tm, test.Picture) 280 | 281 | if got != test.Outputs[i] { 282 | t.Errorf("%s: Expected %q, got %q", test.Picture, test.Outputs[i], got) 283 | } 284 | 285 | if err != nil { 286 | t.Errorf("%s: Expected nil error, got %s", test.Picture, err) 287 | } 288 | } 289 | } 290 | } 291 | 292 | func TestFormatDayOfWeek(t *testing.T) { 293 | 294 | startTime := time.Date(2018, time.April, 1, 12, 0, 0, 0, time.UTC) 295 | 296 | var times [7]time.Time 297 | for i := range times { 298 | times[i] = startTime.AddDate(0, 0, i) 299 | } 300 | 301 | data := []struct { 302 | Picture string 303 | Outputs [7]string 304 | }{ 305 | { 306 | // Default layout is n 307 | Picture: "[F]", 308 | Outputs: [7]string{ 309 | "sunday", 310 | "monday", 311 | "tuesday", 312 | "wednesday", 313 | "thursday", 314 | "friday", 315 | "saturday", 316 | }, 317 | }, 318 | { 319 | Picture: "[FNn]", 320 | Outputs: [7]string{ 321 | "Sunday", 322 | "Monday", 323 | "Tuesday", 324 | "Wednesday", 325 | "Thursday", 326 | "Friday", 327 | "Saturday", 328 | }, 329 | }, 330 | { 331 | Picture: "[FNn,*-6]", 332 | Outputs: [7]string{ 333 | "Sunday", 334 | "Monday", 335 | "Tues", 336 | "Weds", 337 | "Thurs", 338 | "Friday", 339 | "Sat", 340 | }, 341 | }, 342 | { 343 | Picture: "[FNn,6-6]", 344 | Outputs: [7]string{ 345 | "Sunday", 346 | "Monday", 347 | "Tues ", 348 | "Weds ", 349 | "Thurs ", 350 | "Friday", 351 | "Sat ", 352 | }, 353 | }, 354 | { 355 | Picture: "[FNn,*-5]", 356 | Outputs: [7]string{ 357 | "Sun", 358 | "Mon", 359 | "Tues", 360 | "Weds", 361 | "Thurs", 362 | "Fri", 363 | "Sat", 364 | }, 365 | }, 366 | { 367 | Picture: "[FNn,*-4]", 368 | Outputs: [7]string{ 369 | "Sun", 370 | "Mon", 371 | "Tues", 372 | "Weds", 373 | "Thur", 374 | "Fri", 375 | "Sat", 376 | }, 377 | }, 378 | { 379 | Picture: "[FN,*-3]", 380 | Outputs: [7]string{ 381 | "SUN", 382 | "MON", 383 | "TUE", 384 | "WED", 385 | "THU", 386 | "FRI", 387 | "SAT", 388 | }, 389 | }, 390 | { 391 | Picture: "[FNn,*-2]", 392 | Outputs: [7]string{ 393 | "Su", 394 | "Mo", 395 | "Tu", 396 | "We", 397 | "Th", 398 | "Fr", 399 | "Sa", 400 | }, 401 | }, 402 | { 403 | Picture: "Day [F01]: [FNn]", 404 | Outputs: [7]string{ 405 | "Day 01: Sunday", 406 | "Day 02: Monday", 407 | "Day 03: Tuesday", 408 | "Day 04: Wednesday", 409 | "Day 05: Thursday", 410 | "Day 06: Friday", 411 | "Day 07: Saturday", 412 | }, 413 | }, 414 | { 415 | Picture: "[FNn] is the [F1o] day of the week", 416 | Outputs: [7]string{ 417 | "Sunday is the 1st day of the week", 418 | "Monday is the 2nd day of the week", 419 | "Tuesday is the 3rd day of the week", 420 | "Wednesday is the 4th day of the week", 421 | "Thursday is the 5th day of the week", 422 | "Friday is the 6th day of the week", 423 | "Saturday is the 7th day of the week", 424 | }, 425 | }, 426 | { 427 | // Unsupported layouts should fall back to the default. 428 | Picture: "[FI]", 429 | Outputs: [7]string{ 430 | "sunday", 431 | "monday", 432 | "tuesday", 433 | "wednesday", 434 | "thursday", 435 | "friday", 436 | "saturday", 437 | }, 438 | }, 439 | } 440 | 441 | for _, test := range data { 442 | for i, tm := range times { 443 | 444 | got, err := FormatTime(tm, test.Picture) 445 | 446 | if got != test.Outputs[i] { 447 | t.Errorf("%s: Expected %q, got %q", test.Picture, test.Outputs[i], got) 448 | } 449 | 450 | if err != nil { 451 | t.Errorf("%s: Expected nil error, got %s", test.Picture, err) 452 | } 453 | } 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /jlib/jxpath/formatnumber_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jxpath 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | type formatNumberTest struct { 13 | Value float64 14 | Picture string 15 | Output string 16 | Error error 17 | } 18 | 19 | func TestExamples(t *testing.T) { 20 | 21 | tests := []formatNumberTest{ 22 | { 23 | Value: 12345.6, 24 | Picture: "#,###.00", 25 | Output: "12,345.60", 26 | }, 27 | { 28 | Value: 12345678.9, 29 | Picture: "9,999.99", 30 | Output: "12,345,678.90", 31 | }, 32 | { 33 | Value: 123.9, 34 | Picture: "9999", 35 | Output: "0124", 36 | }, 37 | { 38 | Value: 0.14, 39 | Picture: "01%", 40 | Output: "14%", 41 | }, 42 | { 43 | Value: 0.14, 44 | Picture: "001‰", 45 | Output: "140‰", 46 | }, 47 | { 48 | Value: -6, 49 | Picture: "000", 50 | Output: "-006", 51 | }, 52 | { 53 | Value: 1234.5678, 54 | Picture: "#,##0.00", 55 | Output: "1,234.57", 56 | }, 57 | { 58 | Value: 1234.5678, 59 | Picture: "00.000e0", 60 | Output: "12.346e2", 61 | }, 62 | { 63 | Value: 0.234, 64 | Picture: "0.0e0", 65 | Output: "2.3e-1", 66 | }, 67 | { 68 | Value: 0.234, 69 | Picture: "#.00e0", 70 | Output: "0.23e0", 71 | }, 72 | { 73 | Value: 0.234, 74 | Picture: ".00e0", 75 | Output: ".23e0", 76 | }, 77 | } 78 | 79 | testFormatNumber(t, tests) 80 | } 81 | 82 | func testFormatNumber(t *testing.T, tests []formatNumberTest) { 83 | 84 | df := NewDecimalFormat() 85 | 86 | for i, test := range tests { 87 | 88 | output, err := FormatNumber(test.Value, test.Picture, df) 89 | 90 | if output != test.Output { 91 | t.Errorf("%d. FormatNumber(%v, %q): expected %s, got %s", i+1, test.Value, test.Picture, test.Output, output) 92 | } 93 | 94 | if !reflect.DeepEqual(err, test.Error) { 95 | t.Errorf("%d. FormatNumber(%v, %q): expected error %v, got %v", i+1, test.Value, test.Picture, test.Error, err) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /jlib/jxpath/language.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jxpath 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | type dateLanguage struct { 12 | days [7][]string 13 | months [13][]string 14 | am []string 15 | pm []string 16 | tzPrefix string 17 | } 18 | 19 | var dateLanguages = map[string]dateLanguage{ 20 | "en": { 21 | days: [...][]string{ 22 | time.Sunday: { 23 | "Sunday", 24 | "Sun", 25 | "Su", 26 | }, 27 | time.Monday: { 28 | "Monday", 29 | "Mon", 30 | "Mo", 31 | }, 32 | time.Tuesday: { 33 | "Tuesday", 34 | "Tues", 35 | "Tue", 36 | "Tu", 37 | }, 38 | time.Wednesday: { 39 | "Wednesday", 40 | "Weds", 41 | "Wed", 42 | "We", 43 | }, 44 | time.Thursday: { 45 | "Thursday", 46 | "Thurs", 47 | "Thur", 48 | "Thu", 49 | "Th", 50 | }, 51 | time.Friday: { 52 | "Friday", 53 | "Fri", 54 | "Fr", 55 | }, 56 | time.Saturday: { 57 | "Saturday", 58 | "Sat", 59 | "Sa", 60 | }, 61 | }, 62 | months: [...][]string{ 63 | time.January: { 64 | "January", 65 | "Jan", 66 | "Ja", 67 | }, 68 | time.February: { 69 | "February", 70 | "Feb", 71 | "Fe", 72 | }, 73 | time.March: { 74 | "March", 75 | "Mar", 76 | "Mr", 77 | }, 78 | time.April: { 79 | "April", 80 | "Apr", 81 | "Ap", 82 | }, 83 | time.May: { 84 | "May", 85 | "My", 86 | }, 87 | time.June: { 88 | "June", 89 | "Jun", 90 | "Jn", 91 | }, 92 | time.July: { 93 | "July", 94 | "Jul", 95 | "Jl", 96 | }, 97 | time.August: { 98 | "August", 99 | "Aug", 100 | "Au", 101 | }, 102 | time.September: { 103 | "September", 104 | "Sept", 105 | "Sep", 106 | "Se", 107 | }, 108 | time.October: { 109 | "October", 110 | "Oct", 111 | "Oc", 112 | }, 113 | time.November: { 114 | "November", 115 | "Nov", 116 | "No", 117 | }, 118 | time.December: { 119 | "December", 120 | "Dec", 121 | "De", 122 | }, 123 | }, 124 | am: []string{ 125 | "am", 126 | "a", 127 | }, 128 | pm: []string{ 129 | "pm", 130 | "p", 131 | }, 132 | tzPrefix: "GMT", 133 | }, 134 | } 135 | 136 | var defaultLanguage = dateLanguages["en"] 137 | -------------------------------------------------------------------------------- /jlib/number.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jlib 6 | 7 | import ( 8 | "fmt" 9 | "math" 10 | "math/rand" 11 | "reflect" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/blues/jsonata-go/jtypes" 17 | ) 18 | 19 | var reNumber = regexp.MustCompile(`^-?(([0-9]+))(\.[0-9]+)?([Ee][-+]?[0-9]+)?$`) 20 | 21 | // Number converts values to numbers. Numeric values are returned 22 | // unchanged. Strings in legal JSON number format are converted 23 | // to the number they represent. Boooleans are converted to 0 or 1. 24 | // All other types trigger an error. 25 | func Number(value StringNumberBool) (float64, error) { 26 | v := reflect.Value(value) 27 | if b, ok := jtypes.AsBool(v); ok { 28 | if b { 29 | return 1, nil 30 | } 31 | return 0, nil 32 | } 33 | 34 | if n, ok := jtypes.AsNumber(v); ok { 35 | return n, nil 36 | } 37 | 38 | s, ok := jtypes.AsString(v) 39 | if ok && reNumber.MatchString(s) { 40 | if n, err := strconv.ParseFloat(s, 64); err == nil { 41 | return n, nil 42 | } 43 | } 44 | 45 | return 0, fmt.Errorf("unable to cast %q to a number", s) 46 | } 47 | 48 | // Round rounds its input to the number of decimal places given 49 | // in the optional second parameter. By default, Round rounds to 50 | // the nearest integer. A negative precision specifies which column 51 | // to round to on the left hand side of the decimal place. 52 | func Round(x float64, prec jtypes.OptionalInt) float64 { 53 | // Adapted from gonum's floats.RoundEven. 54 | // https://github.com/gonum/gonum/tree/master/floats 55 | 56 | if x == 0 { 57 | // Make sure zero is returned 58 | // without the negative bit set. 59 | return 0 60 | } 61 | // Fast path for positive precision on integers. 62 | if prec.Int >= 0 && x == math.Trunc(x) { 63 | return x 64 | } 65 | intermed := multByPow10(x, prec.Int) 66 | if math.IsInf(intermed, 0) { 67 | return x 68 | } 69 | if isHalfway(intermed) { 70 | correction, _ := math.Modf(math.Mod(intermed, 2)) 71 | intermed += correction 72 | if intermed > 0 { 73 | x = math.Floor(intermed) 74 | } else { 75 | x = math.Ceil(intermed) 76 | } 77 | } else { 78 | if x < 0 { 79 | x = math.Ceil(intermed - 0.5) 80 | } else { 81 | x = math.Floor(intermed + 0.5) 82 | } 83 | } 84 | 85 | if x == 0 { 86 | return 0 87 | } 88 | 89 | return multByPow10(x, -prec.Int) 90 | } 91 | 92 | // Power returns x to the power of y. 93 | func Power(x, y float64) (float64, error) { 94 | res := math.Pow(x, y) 95 | if math.IsInf(res, 0) || math.IsNaN(res) { 96 | return 0, fmt.Errorf("the power function has resulted in a value that cannot be represented as a JSON number") 97 | } 98 | return res, nil 99 | } 100 | 101 | // Sqrt returns the square root of a number. It returns an error 102 | // if the number is less than zero. 103 | func Sqrt(x float64) (float64, error) { 104 | if x < 0 { 105 | return 0, fmt.Errorf("the sqrt function cannot be applied to a negative number") 106 | } 107 | return math.Sqrt(x), nil 108 | } 109 | 110 | // Random returns a random floating point number between 0 and 1. 111 | func Random() float64 { 112 | return rand.Float64() 113 | } 114 | 115 | // multByPow10 multiplies a number by 10 to the power of n. 116 | // It does this by converting back and forth to strings to 117 | // avoid floating point rounding errors, e.g. 118 | // 119 | // 4.525 * math.Pow10(2) returns 452.50000000000006 120 | func multByPow10(x float64, n int) float64 { 121 | if n == 0 || math.IsNaN(x) || math.IsInf(x, 0) { 122 | return x 123 | } 124 | 125 | s := fmt.Sprintf("%g", x) 126 | 127 | chunks := strings.Split(s, "e") 128 | switch len(chunks) { 129 | case 1: 130 | s = chunks[0] + "e" + strconv.Itoa(n) 131 | case 2: 132 | e, _ := strconv.Atoi(chunks[1]) 133 | s = chunks[0] + "e" + strconv.Itoa(e+n) 134 | default: 135 | return x 136 | } 137 | 138 | x, _ = strconv.ParseFloat(s, 64) 139 | return x 140 | } 141 | 142 | func isHalfway(x float64) bool { 143 | _, frac := math.Modf(x) 144 | frac = math.Abs(frac) 145 | return frac == 0.5 || (math.Nextafter(frac, math.Inf(-1)) < 0.5 && math.Nextafter(frac, math.Inf(1)) > 0.5) 146 | } 147 | -------------------------------------------------------------------------------- /jlib/number_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jlib_test 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/blues/jsonata-go/jlib" 12 | "github.com/blues/jsonata-go/jtypes" 13 | ) 14 | 15 | func TestRound(t *testing.T) { 16 | 17 | data := []struct { 18 | Value float64 19 | Precision jtypes.OptionalInt 20 | Output float64 21 | }{ 22 | { 23 | Value: 11.5, 24 | Output: 12, 25 | }, 26 | { 27 | Value: -11.5, 28 | Output: -12, 29 | }, 30 | { 31 | Value: 12.5, 32 | Output: 12, 33 | }, 34 | { 35 | Value: -12.5, 36 | Output: -12, 37 | }, 38 | { 39 | Value: 594.325, 40 | Output: 594, 41 | }, 42 | { 43 | Value: -594.325, 44 | Output: -594, 45 | }, 46 | { 47 | Value: 594.325, 48 | Precision: jtypes.NewOptionalInt(1), 49 | Output: 594.3, 50 | }, 51 | { 52 | Value: -594.325, 53 | Precision: jtypes.NewOptionalInt(1), 54 | Output: -594.3, 55 | }, 56 | { 57 | Value: 594.325, 58 | Precision: jtypes.NewOptionalInt(2), 59 | Output: 594.32, 60 | }, 61 | { 62 | Value: -594.325, 63 | Precision: jtypes.NewOptionalInt(2), 64 | Output: -594.32, 65 | }, 66 | { 67 | Value: 594.325, 68 | Precision: jtypes.NewOptionalInt(3), 69 | Output: 594.325, 70 | }, 71 | { 72 | Value: -594.325, 73 | Precision: jtypes.NewOptionalInt(3), 74 | Output: -594.325, 75 | }, 76 | { 77 | Value: 594.325, 78 | Precision: jtypes.NewOptionalInt(4), 79 | Output: 594.325, 80 | }, 81 | { 82 | Value: -594.325, 83 | Precision: jtypes.NewOptionalInt(4), 84 | Output: -594.325, 85 | }, 86 | { 87 | Value: 594.325, 88 | Precision: jtypes.NewOptionalInt(-1), 89 | Output: 590, 90 | }, 91 | { 92 | Value: -594.325, 93 | Precision: jtypes.NewOptionalInt(-1), 94 | Output: -590, 95 | }, 96 | { 97 | Value: 594.325, 98 | Precision: jtypes.NewOptionalInt(-2), 99 | Output: 600, 100 | }, 101 | { 102 | Value: -594.325, 103 | Precision: jtypes.NewOptionalInt(-2), 104 | Output: -600, 105 | }, 106 | { 107 | Value: 594.325, 108 | Precision: jtypes.NewOptionalInt(-3), 109 | Output: 1000, 110 | }, 111 | { 112 | Value: -594.325, 113 | Precision: jtypes.NewOptionalInt(-3), 114 | Output: -1000, 115 | }, 116 | { 117 | Value: 594.325, 118 | Precision: jtypes.NewOptionalInt(-4), 119 | Output: 0, 120 | }, 121 | { 122 | Value: -594.325, 123 | Precision: jtypes.NewOptionalInt(-4), 124 | Output: 0, 125 | }, 126 | } 127 | 128 | for _, test := range data { 129 | 130 | got := jlib.Round(test.Value, test.Precision) 131 | 132 | if got != test.Output { 133 | 134 | s := fmt.Sprintf("round(%g", test.Value) 135 | if test.Precision.IsSet() { 136 | s += fmt.Sprintf(", %d", test.Precision.Int) 137 | } 138 | s += ")" 139 | 140 | t.Errorf("%s: Expected %g, got %g", s, test.Output, got) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /jparse/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | // Package jparse converts JSONata expressions to abstract 6 | // syntax trees. Most clients will not need to work with 7 | // this package directly. 8 | // 9 | // Usage 10 | // 11 | // Call the Parse function, passing a JSONata expression as 12 | // a string. If an error occurs, it will be of type Error. 13 | // Otherwise, Parse returns the root Node of the AST. 14 | package jparse 15 | -------------------------------------------------------------------------------- /jparse/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jparse 6 | 7 | import ( 8 | "fmt" 9 | "regexp" 10 | ) 11 | 12 | // ErrType describes the type of an error. 13 | type ErrType uint 14 | 15 | // Error types returned by the parser. 16 | const ( 17 | _ ErrType = iota 18 | ErrSyntaxError 19 | ErrUnexpectedEOF 20 | ErrUnexpectedToken 21 | ErrMissingToken 22 | ErrPrefix 23 | ErrInfix 24 | ErrUnterminatedString 25 | ErrUnterminatedRegex 26 | ErrUnterminatedName 27 | ErrIllegalEscape 28 | ErrIllegalEscapeHex 29 | ErrInvalidNumber 30 | ErrNumberRange 31 | ErrEmptyRegex 32 | ErrInvalidRegex 33 | ErrGroupPredicate 34 | ErrGroupGroup 35 | ErrPathLiteral 36 | ErrIllegalAssignment 37 | ErrIllegalParam 38 | ErrDuplicateParam 39 | ErrParamCount 40 | ErrInvalidUnionType 41 | ErrUnmatchedOption 42 | ErrUnmatchedSubtype 43 | ErrInvalidSubtype 44 | ErrInvalidParamType 45 | ) 46 | 47 | var errmsgs = map[ErrType]string{ 48 | ErrSyntaxError: "syntax error: '{{token}}'", 49 | ErrUnexpectedEOF: "unexpected end of expression", 50 | ErrUnexpectedToken: "expected token '{{hint}}', got '{{token}}'", 51 | ErrMissingToken: "expected token '{{hint}}' before end of expression", 52 | ErrPrefix: "the symbol '{{token}}' cannot be used as a prefix operator", 53 | ErrInfix: "the symbol '{{token}}' cannot be used as an infix operator", 54 | ErrUnterminatedString: "unterminated string literal (no closing '{{hint}}')", 55 | ErrUnterminatedRegex: "unterminated regular expression (no closing '{{hint}}')", 56 | ErrUnterminatedName: "unterminated name (no closing '{{hint}}')", 57 | ErrIllegalEscape: "illegal escape sequence \\{{hint}}", 58 | ErrIllegalEscapeHex: "illegal escape sequence \\{{hint}}: \\u must be followed by a 4-digit hexadecimal code point", 59 | ErrInvalidNumber: "invalid number literal {{token}}", 60 | ErrNumberRange: "invalid number literal {{token}}: value out of range", 61 | ErrEmptyRegex: "invalid regular expression: expression cannot be empty", 62 | ErrInvalidRegex: "invalid regular expression {{token}}: {{hint}}", 63 | ErrGroupPredicate: "a predicate cannot follow a grouping expression in a path step", 64 | ErrGroupGroup: "a path step can only have one grouping expression", 65 | ErrPathLiteral: "invalid path step {{hint}}: paths cannot contain nulls, strings, numbers or booleans", 66 | ErrIllegalAssignment: "illegal assignment: {{hint}} is not a variable", 67 | ErrIllegalParam: "illegal function parameter: {{token}} is not a variable", 68 | ErrDuplicateParam: "duplicate function parameter: {{token}}", 69 | ErrParamCount: "invalid type signature: number of types must match number of function parameters", 70 | ErrInvalidUnionType: "invalid type signature: unsupported union type '{{hint}}'", 71 | ErrUnmatchedOption: "invalid type signature: option '{{hint}}' must follow a parameter", 72 | ErrUnmatchedSubtype: "invalid type signature: subtypes must follow a parameter", 73 | ErrInvalidSubtype: "invalid type signature: parameter type {{hint}} does not support subtypes", 74 | ErrInvalidParamType: "invalid type signature: unknown parameter type '{{hint}}'", 75 | } 76 | 77 | var reErrMsg = regexp.MustCompile("{{(token|hint)}}") 78 | 79 | // Error describes an error during parsing. 80 | type Error struct { 81 | Type ErrType 82 | Token string 83 | Hint string 84 | Position int 85 | } 86 | 87 | func newError(typ ErrType, tok token) error { 88 | return newErrorHint(typ, tok, "") 89 | } 90 | 91 | func newErrorHint(typ ErrType, tok token, hint string) error { 92 | return &Error{ 93 | Type: typ, 94 | Token: tok.Value, 95 | Position: tok.Position, 96 | Hint: hint, 97 | } 98 | } 99 | 100 | func (e Error) Error() string { 101 | 102 | s := errmsgs[e.Type] 103 | if s == "" { 104 | return fmt.Sprintf("parser.Error: unknown error type %d", e.Type) 105 | } 106 | 107 | return reErrMsg.ReplaceAllStringFunc(s, func(match string) string { 108 | switch match { 109 | case "{{token}}": 110 | return e.Token 111 | case "{{hint}}": 112 | return e.Hint 113 | default: 114 | return match 115 | } 116 | }) 117 | } 118 | 119 | func panicf(format string, a ...interface{}) { 120 | panic(fmt.Sprintf(format, a...)) 121 | } 122 | -------------------------------------------------------------------------------- /jparse/jparse.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jparse 6 | 7 | // The JSONata parser is based on Pratt's Top Down Operator 8 | // Precededence algorithm (see https://tdop.github.io/). Given 9 | // a series of tokens representing a JSONata expression and the 10 | // following metadata, it converts the tokens into an abstract 11 | // syntax tree: 12 | // 13 | // 1. Functions that convert tokens to nodes based on their 14 | // type and position (see 'nud' and 'led' in Pratt). 15 | // 16 | // 2. Binding powers (i.e. operator precedence values) for 17 | // infix operators (see 'lbp' in Pratt). 18 | // 19 | // This metadata is defined below. 20 | 21 | // A nud (short for null denotation) is a function that takes 22 | // a token and returns a node representing that token's value. 23 | // The parsing algorithm only calls the nud function for tokens 24 | // in the prefix position. This includes simple values like 25 | // strings and numbers, complex values like arrays and objects, 26 | // and prefix operators like the negation operator. 27 | type nud func(*parser, token) (Node, error) 28 | 29 | // An led (short for left denotation) is a function that takes 30 | // a token and a node representing the left hand side of an 31 | // infix operation, and returns a node representing that infix 32 | // operation. The parsing algorithm only calls the led function 33 | // for tokens in the infix position, e.g. the mathematical 34 | // operators. 35 | type led func(*parser, token, Node) (Node, error) 36 | 37 | // nuds defines nud functions for token types that are valid 38 | // in the prefix position. 39 | var nuds = [...]nud{ 40 | typeString: parseString, 41 | typeNumber: parseNumber, 42 | typeBoolean: parseBoolean, 43 | typeNull: parseNull, 44 | typeRegex: parseRegex, 45 | typeVariable: parseVariable, 46 | typeName: parseName, 47 | typeNameEsc: parseEscapedName, 48 | typeBracketOpen: parseArray, 49 | typeBraceOpen: parseObject, 50 | typeParenOpen: parseBlock, 51 | typeMult: parseWildcard, 52 | typeMinus: parseNegation, 53 | typeDescendent: parseDescendent, 54 | typePipe: parseObjectTransformation, 55 | typeIn: parseName, 56 | typeAnd: parseName, 57 | typeOr: parseName, 58 | } 59 | 60 | // leds defines led functions for token types that are valid 61 | // in the infix position. 62 | var leds = [...]led{ 63 | typeParenOpen: parseFunctionCall, 64 | typeBracketOpen: parsePredicate, 65 | typeBraceOpen: parseGroup, 66 | typeCondition: parseConditional, 67 | typeAssign: parseAssignment, 68 | typeApply: parseFunctionApplication, 69 | typeConcat: parseStringConcatenation, 70 | typeSort: parseSort, 71 | typeDot: parseDot, 72 | typePlus: parseNumericOperator, 73 | typeMinus: parseNumericOperator, 74 | typeMult: parseNumericOperator, 75 | typeDiv: parseNumericOperator, 76 | typeMod: parseNumericOperator, 77 | typeEqual: parseComparisonOperator, 78 | typeNotEqual: parseComparisonOperator, 79 | typeLess: parseComparisonOperator, 80 | typeLessEqual: parseComparisonOperator, 81 | typeGreater: parseComparisonOperator, 82 | typeGreaterEqual: parseComparisonOperator, 83 | typeIn: parseComparisonOperator, 84 | typeAnd: parseBooleanOperator, 85 | typeOr: parseBooleanOperator, 86 | } 87 | 88 | // bps defines binding powers for token types that are valid 89 | // in the infix position. The parsing algorithm requires that 90 | // all infix operators (as defined by the leds variable above) 91 | // have a non-zero binding power. 92 | // 93 | // Binding powers are calculated from a 2D slice of token types 94 | // in which the outer slice is ordered by operator precedence 95 | // (highest to lowest) and each inner slice contains token 96 | // types of equal operator precedence. 97 | var bps = initBindingPowers([][]tokenType{ 98 | { 99 | typeParenOpen, 100 | typeBracketOpen, 101 | }, 102 | { 103 | typeDot, 104 | }, 105 | { 106 | typeBraceOpen, 107 | }, 108 | { 109 | typeMult, 110 | typeDiv, 111 | typeMod, 112 | }, 113 | { 114 | typePlus, 115 | typeMinus, 116 | typeConcat, 117 | }, 118 | { 119 | typeEqual, 120 | typeNotEqual, 121 | typeLess, 122 | typeLessEqual, 123 | typeGreater, 124 | typeGreaterEqual, 125 | typeIn, 126 | typeSort, 127 | typeApply, 128 | }, 129 | { 130 | typeAnd, 131 | }, 132 | { 133 | typeOr, 134 | }, 135 | { 136 | typeCondition, 137 | }, 138 | { 139 | typeAssign, 140 | }, 141 | }) 142 | 143 | const ( 144 | nudCount = tokenType(len(nuds)) 145 | ledCount = tokenType(len(leds)) 146 | ) 147 | 148 | func lookupNud(tt tokenType) nud { 149 | if tt >= nudCount { 150 | return nil 151 | } 152 | return nuds[tt] 153 | } 154 | 155 | func lookupLed(tt tokenType) led { 156 | if tt >= ledCount { 157 | return nil 158 | } 159 | return leds[tt] 160 | } 161 | 162 | func lookupBp(tt tokenType) int { 163 | if tt >= ledCount { 164 | return 0 165 | } 166 | return bps[tt] 167 | } 168 | 169 | // Parse builds the abstract syntax tree for a JSONata expression 170 | // and returns the root node. If the provided expression is not 171 | // valid, Parse returns an error of type Error. 172 | func Parse(expr string) (root Node, err error) { 173 | 174 | // Handle panics from parseExpression. 175 | defer func() { 176 | if r := recover(); r != nil { 177 | if e, ok := r.(*Error); ok { 178 | root, err = nil, e 179 | return 180 | } 181 | panic(r) 182 | } 183 | }() 184 | 185 | p := newParser(expr) 186 | node := p.parseExpression(0) 187 | 188 | if p.token.Type != typeEOF { 189 | return nil, newError(ErrSyntaxError, p.token) 190 | } 191 | 192 | return node.optimize() 193 | } 194 | 195 | type parser struct { 196 | lexer lexer 197 | token token 198 | // The following function pointers are a workaround 199 | // for an initialisation loop compile error. See the 200 | // comment in newParser. 201 | lookupNud func(tokenType) nud 202 | lookupLed func(tokenType) led 203 | lookupBp func(tokenType) int 204 | } 205 | 206 | func newParser(input string) parser { 207 | 208 | p := parser{ 209 | lexer: newLexer(input), 210 | 211 | // Because the nuds/leds arrays refer to functions that 212 | // call the parser methods, the parser methods cannot 213 | // directly refer to the nuds/leds arrays. Specifically, 214 | // calling the nud/led lookup functions from the parser 215 | // causes an initialisation loop, e.g. 216 | // 217 | // nuds refers to 218 | // parseArray refers to 219 | // parser.parseExpression refers to 220 | // lookupNud refers to 221 | // nuds 222 | // 223 | // To avoid this, the parser accesses the nud/led lookup 224 | // functions via function pointers set at runtime. 225 | lookupNud: lookupNud, 226 | lookupLed: lookupLed, 227 | lookupBp: lookupBp, 228 | } 229 | 230 | // Set current token to the first token in the expression. 231 | p.advance(true) 232 | return p 233 | } 234 | 235 | // parseExpression is the central function of the Pratt 236 | // algorithm. It handles dispatch to the various nud/led 237 | // functions (which may call back into parseExpression 238 | // and the other parser methods). 239 | // 240 | // Note that the parser methods, parseExpression included, 241 | // panic instead of returning errors. Panics are caught 242 | // by the top-level Parse function and returned to the 243 | // caller as errors. This makes the nud/led functions 244 | // nicer to write without sacrificing the public API. 245 | func (p *parser) parseExpression(rbp int) Node { 246 | 247 | if p.token.Type == typeEOF { 248 | panic(newError(ErrUnexpectedEOF, p.token)) 249 | } 250 | 251 | t := p.token 252 | p.advance(false) 253 | 254 | nud := p.lookupNud(t.Type) 255 | if nud == nil { 256 | panic(newError(ErrPrefix, t)) 257 | } 258 | 259 | lhs, err := nud(p, t) 260 | if err != nil { 261 | panic(err) 262 | } 263 | 264 | for rbp < p.lookupBp(p.token.Type) { 265 | 266 | t := p.token 267 | p.advance(true) 268 | 269 | led := p.lookupLed(t.Type) 270 | if led == nil { 271 | panic(newError(ErrInfix, t)) 272 | } 273 | 274 | lhs, err = led(p, t, lhs) 275 | if err != nil { 276 | panic(err) 277 | } 278 | } 279 | 280 | return lhs 281 | } 282 | 283 | // advance requests the next token from the lexer and updates 284 | // the parser's current token pointer. It panics if the lexer 285 | // returns an error token. 286 | func (p *parser) advance(allowRegex bool) { 287 | p.token = p.lexer.next(allowRegex) 288 | if p.token.Type == typeError { 289 | panic(p.lexer.err) 290 | } 291 | } 292 | 293 | // consume is like advance except it first checks that the 294 | // current token is of the expected type. It panics if that 295 | // is not the case. 296 | func (p *parser) consume(expected tokenType, allowRegex bool) { 297 | 298 | if p.token.Type != expected { 299 | 300 | typ := ErrUnexpectedToken 301 | if p.token.Type == typeEOF { 302 | typ = ErrMissingToken 303 | } 304 | 305 | panic(newErrorHint(typ, p.token, expected.String())) 306 | } 307 | 308 | p.advance(allowRegex) 309 | } 310 | 311 | // bp returns the binding power for the given token type. 312 | func (p *parser) bp(t tokenType) int { 313 | return p.lookupBp(t) 314 | } 315 | 316 | // initBindingPowers calculates binding power values for the 317 | // given token types and returns them as an array. The specific 318 | // values are not important. All that matters for parsing is 319 | // whether one token's binding power is higher than another's. 320 | // 321 | // Token types are provided as a slice of slices. The outer 322 | // slice is ordered by operator precedence, highest to lowest. 323 | // Token types within each inner slice have the same operator 324 | // precedence. 325 | func initBindingPowers(tokenTypes [][]tokenType) [ledCount]int { 326 | 327 | // Binding powers must: 328 | // 329 | // 1. be non-zero 330 | // 2. increase with operator precedence 331 | // 3. be separated by more than one (because we subtract 332 | // 1 from the binding power for right-associative 333 | // operators). 334 | // 335 | // This function produces a minimum binding power of 10. 336 | // Values increase by 10 as operator precedence increases. 337 | 338 | var bps [ledCount]int 339 | 340 | for offset, tts := range tokenTypes { 341 | 342 | bp := (len(tokenTypes) - offset) * 10 343 | 344 | for _, tt := range tts { 345 | if bps[tt] != 0 { 346 | panicf("initBindingPowers: token type %d [%s] appears more than once", tt, tt) 347 | } 348 | bps[tt] = bp 349 | } 350 | } 351 | 352 | validateBindingPowers(bps) 353 | 354 | return bps 355 | } 356 | 357 | // validateBindingPowers sanity checks the values calculated 358 | // by initBindingPowers. Every token type in the leds array 359 | // should have a binding power. No other token type should 360 | // have a binding power. 361 | func validateBindingPowers(bps [ledCount]int) { 362 | 363 | for tt := tokenType(0); tt < ledCount; tt++ { 364 | if leds[tt] != nil && bps[tt] == 0 { 365 | panicf("validateBindingPowers: token type %d [%s] does not have a binding power", tt, tt) 366 | } 367 | if leds[tt] == nil && bps[tt] != 0 { 368 | panicf("validateBindingPowers: token type %d [%s] should not have a binding power", tt, tt) 369 | } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /jparse/lexer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jparse 6 | 7 | import ( 8 | "fmt" 9 | "unicode/utf8" 10 | ) 11 | 12 | const eof = -1 13 | 14 | type tokenType uint8 15 | 16 | const ( 17 | typeEOF tokenType = iota 18 | typeError 19 | 20 | typeString // string literal, e.g. "hello" 21 | typeNumber // number literal, e.g. 3.14159 22 | typeBoolean // true or false 23 | typeNull // null 24 | typeName // field name, e.g. Price 25 | typeNameEsc // escaped field name, e.g. `Product Name` 26 | typeVariable // variable, e.g. $x 27 | typeRegex // regular expression, e.g. /ab+/ 28 | 29 | // Symbol operators 30 | typeBracketOpen 31 | typeBracketClose 32 | typeBraceOpen 33 | typeBraceClose 34 | typeParenOpen 35 | typeParenClose 36 | typeDot 37 | typeComma 38 | typeColon 39 | typeSemicolon 40 | typeCondition 41 | typePlus 42 | typeMinus 43 | typeMult 44 | typeDiv 45 | typeMod 46 | typePipe 47 | typeEqual 48 | typeNotEqual 49 | typeLess 50 | typeLessEqual 51 | typeGreater 52 | typeGreaterEqual 53 | typeApply 54 | typeSort 55 | typeConcat 56 | typeRange 57 | typeAssign 58 | typeDescendent 59 | 60 | // Keyword operators 61 | typeAnd 62 | typeOr 63 | typeIn 64 | ) 65 | 66 | func (tt tokenType) String() string { 67 | switch tt { 68 | case typeEOF: 69 | return "(eof)" 70 | case typeError: 71 | return "(error)" 72 | case typeString: 73 | return "(string)" 74 | case typeNumber: 75 | return "(number)" 76 | case typeBoolean: 77 | return "(boolean)" 78 | case typeName, typeNameEsc: 79 | return "(name)" 80 | case typeVariable: 81 | return "(variable)" 82 | case typeRegex: 83 | return "(regex)" 84 | default: 85 | if s := symbolsAndKeywords[tt]; s != "" { 86 | return s 87 | } 88 | return "(unknown)" 89 | } 90 | } 91 | 92 | // symbols1 maps 1-character symbols to the corresponding 93 | // token types. 94 | var symbols1 = [...]tokenType{ 95 | '[': typeBracketOpen, 96 | ']': typeBracketClose, 97 | '{': typeBraceOpen, 98 | '}': typeBraceClose, 99 | '(': typeParenOpen, 100 | ')': typeParenClose, 101 | '.': typeDot, 102 | ',': typeComma, 103 | ';': typeSemicolon, 104 | ':': typeColon, 105 | '?': typeCondition, 106 | '+': typePlus, 107 | '-': typeMinus, 108 | '*': typeMult, 109 | '/': typeDiv, 110 | '%': typeMod, 111 | '|': typePipe, 112 | '=': typeEqual, 113 | '<': typeLess, 114 | '>': typeGreater, 115 | '^': typeSort, 116 | '&': typeConcat, 117 | } 118 | 119 | type runeTokenType struct { 120 | r rune 121 | tt tokenType 122 | } 123 | 124 | // symbols2 maps 2-character symbols to the corresponding 125 | // token types. 126 | var symbols2 = [...][]runeTokenType{ 127 | '!': {{'=', typeNotEqual}}, 128 | '<': {{'=', typeLessEqual}}, 129 | '>': {{'=', typeGreaterEqual}}, 130 | '.': {{'.', typeRange}}, 131 | '~': {{'>', typeApply}}, 132 | ':': {{'=', typeAssign}}, 133 | '*': {{'*', typeDescendent}}, 134 | } 135 | 136 | const ( 137 | symbol1Count = rune(len(symbols1)) 138 | symbol2Count = rune(len(symbols2)) 139 | ) 140 | 141 | func lookupSymbol1(r rune) tokenType { 142 | if r < 0 || r >= symbol1Count { 143 | return 0 144 | } 145 | return symbols1[r] 146 | } 147 | 148 | func lookupSymbol2(r rune) []runeTokenType { 149 | if r < 0 || r >= symbol2Count { 150 | return nil 151 | } 152 | return symbols2[r] 153 | } 154 | 155 | func lookupKeyword(s string) tokenType { 156 | switch s { 157 | case "and": 158 | return typeAnd 159 | case "or": 160 | return typeOr 161 | case "in": 162 | return typeIn 163 | case "true", "false": 164 | return typeBoolean 165 | case "null": 166 | return typeNull 167 | default: 168 | return 0 169 | } 170 | } 171 | 172 | // A token represents a discrete part of a JSONata expression 173 | // such as a string, a number, a field name, or an operator. 174 | type token struct { 175 | Type tokenType 176 | Value string 177 | Position int 178 | } 179 | 180 | // lexer converts a JSONata expression into a sequence of tokens. 181 | // The implmentation is based on the technique described in Rob 182 | // Pike's 'Lexical Scanning in Go' talk. 183 | type lexer struct { 184 | input string 185 | length int 186 | start int 187 | current int 188 | width int 189 | err error 190 | } 191 | 192 | // newLexer creates a new lexer from the provided input. The 193 | // input is tokenized by successive calls to the next method. 194 | func newLexer(input string) lexer { 195 | return lexer{ 196 | input: input, 197 | length: len(input), 198 | } 199 | } 200 | 201 | // next returns the next token from the provided input. When 202 | // the end of the input is reached, next returns EOF for all 203 | // subsequent calls. 204 | // 205 | // The allowRegex argument determines how the lexer interprets 206 | // a forward slash character. Forward slashes in JSONata can 207 | // either be the start of a regular expression or the division 208 | // operator depending on their position. If allowRegex is true, 209 | // the lexer will treat a forward slash like a regular 210 | // expression. 211 | func (l *lexer) next(allowRegex bool) token { 212 | 213 | l.skipWhitespace() 214 | 215 | ch := l.nextRune() 216 | if ch == eof { 217 | return l.eof() 218 | } 219 | 220 | if allowRegex && ch == '/' { 221 | l.ignore() 222 | return l.scanRegex(ch) 223 | } 224 | 225 | if rts := lookupSymbol2(ch); rts != nil { 226 | for _, rt := range rts { 227 | if l.acceptRune(rt.r) { 228 | return l.newToken(rt.tt) 229 | } 230 | } 231 | } 232 | 233 | if tt := lookupSymbol1(ch); tt > 0 { 234 | return l.newToken(tt) 235 | } 236 | 237 | if ch == '"' || ch == '\'' { 238 | l.ignore() 239 | return l.scanString(ch) 240 | } 241 | 242 | if ch >= '0' && ch <= '9' { 243 | l.backup() 244 | return l.scanNumber() 245 | } 246 | 247 | if ch == '`' { 248 | l.ignore() 249 | return l.scanEscapedName(ch) 250 | } 251 | 252 | l.backup() 253 | return l.scanName() 254 | } 255 | 256 | // scanRegex reads a regular expression from the current position 257 | // and returns a regex token. The opening delimiter has already 258 | // been consumed. 259 | func (l *lexer) scanRegex(delim rune) token { 260 | 261 | var depth int 262 | 263 | Loop: 264 | for { 265 | switch l.nextRune() { 266 | case delim: 267 | if depth == 0 { 268 | break Loop 269 | } 270 | case '(', '[', '{': 271 | depth++ 272 | case ')', ']', '}': 273 | depth-- 274 | case '\\': 275 | if r := l.nextRune(); r != eof && r != '\n' { 276 | break 277 | } 278 | fallthrough 279 | case eof, '\n': 280 | return l.error(ErrUnterminatedRegex, string(delim)) 281 | } 282 | } 283 | 284 | l.backup() 285 | t := l.newToken(typeRegex) 286 | l.acceptRune(delim) 287 | l.ignore() 288 | 289 | // Convert JavaScript-style regex flags to Go format, 290 | // e.g. /ab+/i becomes /(?i)ab+/. 291 | if l.acceptAll(isRegexFlag) { 292 | flags := l.newToken(0) 293 | t.Value = fmt.Sprintf("(?%s)%s", flags.Value, t.Value) 294 | } 295 | 296 | return t 297 | } 298 | 299 | // scanString reads a string literal from the current position 300 | // and returns a string token. The opening quote has already been 301 | // consumed. 302 | func (l *lexer) scanString(quote rune) token { 303 | Loop: 304 | for { 305 | switch l.nextRune() { 306 | case quote: 307 | break Loop 308 | case '\\': 309 | if r := l.nextRune(); r != eof { 310 | break 311 | } 312 | fallthrough 313 | case eof: 314 | return l.error(ErrUnterminatedString, string(quote)) 315 | } 316 | } 317 | 318 | l.backup() 319 | t := l.newToken(typeString) 320 | l.acceptRune(quote) 321 | l.ignore() 322 | return t 323 | } 324 | 325 | // scanNumber reads a number literal from the current position 326 | // and returns a number token. 327 | func (l *lexer) scanNumber() token { 328 | 329 | // JSON does not support leading zeroes. The integer part of 330 | // a number will either be a single zero, or a non-zero digit 331 | // followed by zero or more digits. 332 | if !l.acceptRune('0') { 333 | l.accept(isNonZeroDigit) 334 | l.acceptAll(isDigit) 335 | } 336 | if l.acceptRune('.') { 337 | if !l.acceptAll(isDigit) { 338 | // If there are no digits after the decimal point, 339 | // don't treat the dot as part of the number. It 340 | // could be part of the range operator, e.g. "1..5". 341 | l.backup() 342 | return l.newToken(typeNumber) 343 | } 344 | } 345 | if l.acceptRunes2('e', 'E') { 346 | l.acceptRunes2('+', '-') 347 | l.acceptAll(isDigit) 348 | } 349 | return l.newToken(typeNumber) 350 | } 351 | 352 | // scanEscapedName reads a field name from the current position 353 | // and returns a name token. The opening quote has already been 354 | // consumed. 355 | func (l *lexer) scanEscapedName(quote rune) token { 356 | Loop: 357 | for { 358 | switch l.nextRune() { 359 | case quote: 360 | break Loop 361 | case eof, '\n': 362 | return l.error(ErrUnterminatedName, string(quote)) 363 | } 364 | } 365 | 366 | l.backup() 367 | t := l.newToken(typeNameEsc) 368 | l.acceptRune(quote) 369 | l.ignore() 370 | return t 371 | } 372 | 373 | // scanName reads from the current position and returns a name, 374 | // variable, or keyword token. 375 | func (l *lexer) scanName() token { 376 | 377 | isVar := l.acceptRune('$') 378 | if isVar { 379 | l.ignore() 380 | } 381 | 382 | for { 383 | ch := l.nextRune() 384 | if ch == eof { 385 | break 386 | } 387 | 388 | // Stop reading if we hit whitespace... 389 | if isWhitespace(ch) { 390 | l.backup() 391 | break 392 | } 393 | 394 | // ...or anything that looks like an operator. 395 | if lookupSymbol1(ch) > 0 || lookupSymbol2(ch) != nil { 396 | l.backup() 397 | break 398 | } 399 | } 400 | 401 | t := l.newToken(typeName) 402 | 403 | if isVar { 404 | t.Type = typeVariable 405 | } else if tt := lookupKeyword(t.Value); tt > 0 { 406 | t.Type = tt 407 | } 408 | 409 | return t 410 | } 411 | 412 | func (l *lexer) eof() token { 413 | return token{ 414 | Type: typeEOF, 415 | Position: l.current, 416 | } 417 | } 418 | 419 | func (l *lexer) error(typ ErrType, hint string) token { 420 | t := l.newToken(typeError) 421 | l.err = newErrorHint(typ, t, hint) 422 | return t 423 | } 424 | 425 | func (l *lexer) newToken(tt tokenType) token { 426 | t := token{ 427 | Type: tt, 428 | Value: l.input[l.start:l.current], 429 | Position: l.start, 430 | } 431 | l.width = 0 432 | l.start = l.current 433 | return t 434 | } 435 | 436 | func (l *lexer) nextRune() rune { 437 | 438 | if l.err != nil || l.current >= l.length { 439 | l.width = 0 440 | return eof 441 | } 442 | 443 | r, w := utf8.DecodeRuneInString(l.input[l.current:]) 444 | l.width = w 445 | l.current += w 446 | /* 447 | if r == '\n' { 448 | l.line++ 449 | } 450 | */ 451 | return r 452 | } 453 | 454 | func (l *lexer) backup() { 455 | // TODO: Support more than one backup operation. 456 | // TODO: Store current rune so that when nextRune 457 | // is called again, we don't need to repeat the call 458 | // to DecodeRuneInString. 459 | l.current -= l.width 460 | } 461 | 462 | func (l *lexer) ignore() { 463 | l.start = l.current 464 | } 465 | 466 | func (l *lexer) acceptRune(r rune) bool { 467 | return l.accept(func(c rune) bool { 468 | return c == r 469 | }) 470 | } 471 | 472 | func (l *lexer) acceptRunes2(r1, r2 rune) bool { 473 | return l.accept(func(c rune) bool { 474 | return c == r1 || c == r2 475 | }) 476 | } 477 | 478 | func (l *lexer) accept(isValid func(rune) bool) bool { 479 | if isValid(l.nextRune()) { 480 | return true 481 | } 482 | l.backup() 483 | return false 484 | } 485 | 486 | func (l *lexer) acceptAll(isValid func(rune) bool) bool { 487 | var b bool 488 | for l.accept(isValid) { 489 | b = true 490 | } 491 | return b 492 | } 493 | 494 | func (l *lexer) skipWhitespace() { 495 | l.acceptAll(isWhitespace) 496 | l.ignore() 497 | } 498 | 499 | func isWhitespace(r rune) bool { 500 | switch r { 501 | case ' ', '\t', '\n', '\r', '\v': 502 | return true 503 | default: 504 | return false 505 | } 506 | } 507 | 508 | func isRegexFlag(r rune) bool { 509 | switch r { 510 | case 'i', 'm', 's': 511 | return true 512 | default: 513 | return false 514 | } 515 | } 516 | 517 | func isDigit(r rune) bool { 518 | return r >= '0' && r <= '9' 519 | } 520 | 521 | func isNonZeroDigit(r rune) bool { 522 | return r >= '1' && r <= '9' 523 | } 524 | 525 | // symbolsAndKeywords maps operator token types back to their 526 | // string representations. It's only used by tokenType.String 527 | // (and one test). 528 | var symbolsAndKeywords = func() map[tokenType]string { 529 | 530 | m := map[tokenType]string{ 531 | typeAnd: "and", 532 | typeOr: "or", 533 | typeIn: "in", 534 | typeNull: "null", 535 | } 536 | 537 | for r, tt := range symbols1 { 538 | if tt > 0 { 539 | m[tt] = fmt.Sprintf("%c", r) 540 | } 541 | } 542 | 543 | for r, rts := range symbols2 { 544 | for _, rt := range rts { 545 | m[rt.tt] = fmt.Sprintf("%c", r) + fmt.Sprintf("%c", rt.r) 546 | } 547 | } 548 | 549 | return m 550 | }() 551 | -------------------------------------------------------------------------------- /jparse/lexer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jparse 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | type lexerTestCase struct { 13 | Input string 14 | AllowRegex bool 15 | Tokens []token 16 | Error error 17 | } 18 | 19 | func TestLexerWhitespace(t *testing.T) { 20 | testLexer(t, []lexerTestCase{ 21 | { 22 | Input: "", 23 | }, 24 | { 25 | Input: " ", 26 | }, 27 | { 28 | Input: "\v\t\r\n", 29 | }, 30 | { 31 | Input: ` 32 | 33 | 34 | `, 35 | }, 36 | }) 37 | } 38 | 39 | func TestLexerRegex(t *testing.T) { 40 | testLexer(t, []lexerTestCase{ 41 | { 42 | Input: `//`, 43 | Tokens: []token{ 44 | tok(typeDiv, "/", 0), 45 | tok(typeDiv, "/", 1), 46 | }, 47 | }, 48 | { 49 | Input: `//`, 50 | AllowRegex: true, 51 | Tokens: []token{ 52 | tok(typeRegex, "", 1), 53 | }, 54 | }, 55 | { 56 | Input: `/ab+/`, 57 | AllowRegex: true, 58 | Tokens: []token{ 59 | tok(typeRegex, "ab+", 1), 60 | }, 61 | }, 62 | { 63 | Input: `/(ab+/)/`, 64 | AllowRegex: true, 65 | Tokens: []token{ 66 | tok(typeRegex, "(ab+/)", 1), 67 | }, 68 | }, 69 | { 70 | Input: `/ab+/i`, 71 | AllowRegex: true, 72 | Tokens: []token{ 73 | tok(typeRegex, "(?i)ab+", 1), 74 | }, 75 | }, 76 | { 77 | Input: `/ab+/ i`, 78 | AllowRegex: true, 79 | Tokens: []token{ 80 | tok(typeRegex, "ab+", 1), 81 | tok(typeName, "i", 6), 82 | }, 83 | }, 84 | { 85 | Input: `/ab+/I`, 86 | AllowRegex: true, 87 | Tokens: []token{ 88 | tok(typeRegex, "ab+", 1), 89 | tok(typeName, "I", 5), 90 | }, 91 | }, 92 | { 93 | Input: `/ab+`, 94 | AllowRegex: true, 95 | Tokens: []token{ 96 | tok(typeError, "ab+", 1), 97 | }, 98 | Error: &Error{ 99 | Type: ErrUnterminatedRegex, 100 | Token: "ab+", 101 | Hint: "/", 102 | Position: 1, 103 | }, 104 | }, 105 | }) 106 | } 107 | 108 | func TestLexerStrings(t *testing.T) { 109 | testLexer(t, []lexerTestCase{ 110 | { 111 | Input: `""`, 112 | Tokens: []token{ 113 | tok(typeString, "", 1), 114 | }, 115 | }, 116 | { 117 | Input: `''`, 118 | Tokens: []token{ 119 | tok(typeString, "", 1), 120 | }, 121 | }, 122 | { 123 | Input: `"double quotes"`, 124 | Tokens: []token{ 125 | tok(typeString, "double quotes", 1), 126 | }, 127 | }, 128 | { 129 | Input: "'single quotes'", 130 | Tokens: []token{ 131 | tok(typeString, "single quotes", 1), 132 | }, 133 | }, 134 | { 135 | Input: `"escape\t"`, 136 | Tokens: []token{ 137 | tok(typeString, "escape\\t", 1), 138 | }, 139 | }, 140 | { 141 | Input: `'escape\u0036'`, 142 | Tokens: []token{ 143 | tok(typeString, "escape\\u0036", 1), 144 | }, 145 | }, 146 | { 147 | Input: `"超明體繁"`, 148 | Tokens: []token{ 149 | tok(typeString, "超明體繁", 1), 150 | }, 151 | }, 152 | { 153 | Input: `'日本語'`, 154 | Tokens: []token{ 155 | tok(typeString, "日本語", 1), 156 | }, 157 | }, 158 | { 159 | Input: `"No closing quote...`, 160 | Tokens: []token{ 161 | tok(typeError, "No closing quote...", 1), 162 | }, 163 | Error: &Error{ 164 | Type: ErrUnterminatedString, 165 | Token: "No closing quote...", 166 | Hint: "\"", 167 | Position: 1, 168 | }, 169 | }, 170 | { 171 | Input: `'No closing quote...`, 172 | Tokens: []token{ 173 | tok(typeError, "No closing quote...", 1), 174 | }, 175 | Error: &Error{ 176 | Type: ErrUnterminatedString, 177 | Token: "No closing quote...", 178 | Hint: "'", 179 | Position: 1, 180 | }, 181 | }, 182 | }) 183 | } 184 | 185 | func TestLexerNumbers(t *testing.T) { 186 | testLexer(t, []lexerTestCase{ 187 | { 188 | Input: "1", 189 | Tokens: []token{ 190 | tok(typeNumber, "1", 0), 191 | }, 192 | }, 193 | { 194 | Input: "3.14159", 195 | Tokens: []token{ 196 | tok(typeNumber, "3.14159", 0), 197 | }, 198 | }, 199 | { 200 | Input: "1e10", 201 | Tokens: []token{ 202 | tok(typeNumber, "1e10", 0), 203 | }, 204 | }, 205 | { 206 | Input: "1E-10", 207 | Tokens: []token{ 208 | tok(typeNumber, "1E-10", 0), 209 | }, 210 | }, 211 | { 212 | // Signs are separate tokens. 213 | Input: "-100", 214 | Tokens: []token{ 215 | tok(typeMinus, "-", 0), 216 | tok(typeNumber, "100", 1), 217 | }, 218 | }, 219 | { 220 | // Leading zeroes are not supported. 221 | Input: "007", 222 | Tokens: []token{ 223 | tok(typeNumber, "0", 0), 224 | tok(typeNumber, "0", 1), 225 | tok(typeNumber, "7", 2), 226 | }, 227 | }, 228 | { 229 | // Leading decimal points are not supported. 230 | Input: ".5", 231 | Tokens: []token{ 232 | tok(typeDot, ".", 0), 233 | tok(typeNumber, "5", 1), 234 | }, 235 | }, 236 | { 237 | // Trailing decimal points are not supported. 238 | // TODO: Why does this require a character following the decimal point? 239 | Input: "5. ", 240 | Tokens: []token{ 241 | tok(typeNumber, "5", 0), 242 | tok(typeDot, ".", 1), 243 | }, 244 | }, 245 | }) 246 | } 247 | 248 | func TestLexerNames(t *testing.T) { 249 | testLexer(t, []lexerTestCase{ 250 | { 251 | Input: "hello", 252 | Tokens: []token{ 253 | tok(typeName, "hello", 0), 254 | }, 255 | }, 256 | { 257 | // Names break at whitespace... 258 | Input: "hello world", 259 | Tokens: []token{ 260 | tok(typeName, "hello", 0), 261 | tok(typeName, "world", 6), 262 | }, 263 | }, 264 | { 265 | // ...and anything that looks like a symbol. 266 | Input: "hello, world.", 267 | Tokens: []token{ 268 | tok(typeName, "hello", 0), 269 | tok(typeComma, ",", 5), 270 | tok(typeName, "world", 7), 271 | tok(typeDot, ".", 12), 272 | }, 273 | }, 274 | { 275 | // Exclamation marks are not symbols but the != operator 276 | // begins with one so it has the same effect on a name. 277 | Input: "HELLO!", 278 | Tokens: []token{ 279 | tok(typeName, "HELLO", 0), 280 | tok(typeName, "!", 5), 281 | }, 282 | }, 283 | { 284 | // Escaped names can contain whitespace, symbols... 285 | Input: "`hello, world.`", 286 | Tokens: []token{ 287 | tok(typeNameEsc, "hello, world.", 1), 288 | }, 289 | }, 290 | { 291 | // ...and keywords. 292 | Input: "`true or false`", 293 | Tokens: []token{ 294 | tok(typeNameEsc, "true or false", 1), 295 | }, 296 | }, 297 | { 298 | Input: "`no closing quote...", 299 | Tokens: []token{ 300 | tok(typeError, "no closing quote...", 1), 301 | }, 302 | Error: &Error{ 303 | Type: ErrUnterminatedName, 304 | Token: "no closing quote...", 305 | Hint: "`", 306 | Position: 1, 307 | }, 308 | }, 309 | }) 310 | } 311 | 312 | func TestLexerVariables(t *testing.T) { 313 | testLexer(t, []lexerTestCase{ 314 | { 315 | Input: "$", 316 | Tokens: []token{ 317 | tok(typeVariable, "", 1), 318 | }, 319 | }, 320 | { 321 | Input: "$$", 322 | Tokens: []token{ 323 | tok(typeVariable, "$", 1), 324 | }, 325 | }, 326 | { 327 | Input: "$var", 328 | Tokens: []token{ 329 | tok(typeVariable, "var", 1), 330 | }, 331 | }, 332 | { 333 | Input: "$uppercase", 334 | Tokens: []token{ 335 | tok(typeVariable, "uppercase", 1), 336 | }, 337 | }, 338 | }) 339 | } 340 | 341 | func TestLexerSymbolsAndKeywords(t *testing.T) { 342 | 343 | var tests []lexerTestCase 344 | 345 | for tt, s := range symbolsAndKeywords { 346 | tests = append(tests, lexerTestCase{ 347 | Input: s, 348 | Tokens: []token{ 349 | tok(tt, s, 0), 350 | }, 351 | }) 352 | } 353 | 354 | testLexer(t, tests) 355 | } 356 | 357 | func testLexer(t *testing.T, data []lexerTestCase) { 358 | 359 | for _, test := range data { 360 | 361 | l := newLexer(test.Input) 362 | eof := tok(typeEOF, "", len(test.Input)) 363 | 364 | for _, exp := range test.Tokens { 365 | compareTokens(t, test.Input, exp, l.next(test.AllowRegex)) 366 | } 367 | 368 | compareErrors(t, test.Input, test.Error, l.err) 369 | 370 | // The lexer should keep returning EOF after exhausting 371 | // the input. Call next() a few times to make sure that 372 | // repeated calls return EOF as expected. 373 | for i := 0; i < 3; i++ { 374 | compareTokens(t, test.Input, eof, l.next(test.AllowRegex)) 375 | } 376 | } 377 | } 378 | 379 | func compareTokens(t *testing.T, prefix string, exp, got token) { 380 | 381 | if got.Type != exp.Type { 382 | t.Errorf("%s: expected token with Type '%s', got '%s'", prefix, exp.Type, got.Type) 383 | } 384 | 385 | if got.Value != exp.Value { 386 | t.Errorf("%s: expected token with Value %q, got %q", prefix, exp.Value, got.Value) 387 | } 388 | 389 | if got.Position != exp.Position { 390 | t.Errorf("%s: expected token with Position %d, got %d", prefix, exp.Position, got.Position) 391 | } 392 | } 393 | 394 | func compareErrors(t *testing.T, prefix string, exp, got error) { 395 | 396 | if !reflect.DeepEqual(exp, got) { 397 | t.Errorf("%s: expected error %v, got %v", prefix, exp, got) 398 | } 399 | } 400 | 401 | func tok(typ tokenType, value string, position int) token { 402 | return token{ 403 | Type: typ, 404 | Value: value, 405 | Position: position, 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /jsonata-server/.gitignore: -------------------------------------------------------------------------------- 1 | # Executables 2 | jsonata-server 3 | *.exe -------------------------------------------------------------------------------- /jsonata-server/README.md: -------------------------------------------------------------------------------- 1 | # JSONata Server 2 | 3 | A locally hosted version of [JSONata Exerciser](http://try.jsonata.org/) 4 | for testing [jsonata-go](https://github.com/blues/jsonata). 5 | 6 | ## Install 7 | 8 | go install github.com/blues/jsonata-go/jsonata-server 9 | 10 | ## Usage 11 | 12 | $ jsonata-server [-port=] 13 | 14 | Then go to http://localhost:8080/ (or your preferred port number). 15 | -------------------------------------------------------------------------------- /jsonata-server/bench.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "log" 9 | "net/http" 10 | 11 | "encoding/json" 12 | 13 | jsonata "github.com/blues/jsonata-go" 14 | ) 15 | 16 | var ( 17 | benchData = []byte(` 18 | { 19 | "req":"note.add", 20 | "device":"sim32-1232353453453452346", 21 | "app":"test1", 22 | "file":"geiger.q", 23 | "note":"abc123", 24 | "by":"1", 25 | "when":1512335179, 26 | "where":"87JFH688+2GP", 27 | "payload":"SGVsbG8sIHdvcmxkLg==", 28 | "body": 29 | { 30 | "loc_olc":"87JFH688+2GP", 31 | "env_temp":9.407184, 32 | "env_humid":77.071495, 33 | "env_press":1016.25323, 34 | "bat_voltage":3.866328, 35 | "bat_current":0.078125, 36 | "bat_charge":64.42578, 37 | "lnd_7318u":27.6, 38 | "lnd_7318c":23.1, 39 | "lnd_7128ec":9.3, 40 | "pms_pm01_0":0, 41 | "pms_pm02_5":0, 42 | "pms_pm10_0":1, 43 | "pms_c00_30":11076, 44 | "pms_c00_50":3242, 45 | "pms_c01_00":246, 46 | "pms_c02_50":44, 47 | "pms_c05_00":10, 48 | "pms_c10_00":10, 49 | "pms_csecs":118, 50 | "opc_pm01_0":1.9840136, 51 | "opc_pm02_5":3.9194343, 52 | "opc_pm10_0":9.284608, 53 | "opc_c00_38":139, 54 | "opc_c00_54":154, 55 | "opc_c01_00":121, 56 | "opc_c02_10":30, 57 | "opc_c05_00":3, 58 | "opc_c10_00":0, 59 | "opc_csecs":120 60 | } 61 | }`) 62 | 63 | benchExpression = ` 64 | ( 65 | $values := { 66 | "device_uid": device, 67 | "when_captured": $formatTime(when), 68 | "loc_lat": $latitudeFromOLC(body.loc_olc), 69 | "loc_lon": $longitudeFromOLC(body.loc_olc) 70 | }; 71 | 72 | req = "note.add" and when ? $merge([body, $values]) : $error("unexpected req/when") 73 | )` 74 | ) 75 | 76 | // Decode the JSON. 77 | var data interface{} 78 | 79 | func init() { 80 | if err := json.Unmarshal(benchData, &data); err != nil { 81 | panic(err) 82 | } 83 | } 84 | 85 | func benchmark(w http.ResponseWriter, r *http.Request) { 86 | 87 | // Compile the JSONata expression. 88 | expr, err := jsonata.Compile(benchExpression) 89 | if err != nil { 90 | bencherr(w, err) 91 | } 92 | 93 | // Evaluate the JSONata expression. 94 | _, err = expr.Eval(data) 95 | if err != nil { 96 | bencherr(w, err) 97 | } 98 | 99 | if _, err := w.Write([]byte("success")); err != nil { 100 | log.Fatal(err) 101 | } 102 | } 103 | 104 | func bencherr(w http.ResponseWriter, err error) { 105 | log.Println(err) 106 | http.Error(w, err.Error(), http.StatusInternalServerError) 107 | 108 | } 109 | -------------------------------------------------------------------------------- /jsonata-server/exts.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/blues/jsonata-go/jlib" 9 | "github.com/blues/jsonata-go/jtypes" 10 | ) 11 | 12 | // Default format for dates: e.g. 2006-01-02 15:04 MST 13 | const defaultDateFormat = "[Y]-[M01]-[D01] [H01]:[m] [ZN]" 14 | 15 | // formatTime converts a unix time in seconds to a string with the 16 | // given layout. If a time zone is provided, formatTime returns a 17 | // timestamp with that time zone. Otherwise, it returns UTC time. 18 | func formatTime(secs int64, picture jtypes.OptionalString, tz jtypes.OptionalString) (string, error) { 19 | 20 | if picture.String == "" { 21 | picture = jtypes.NewOptionalString(defaultDateFormat) 22 | } 23 | 24 | return jlib.FromMillis(secs*1000, picture, tz) 25 | } 26 | 27 | // parseTime converts a timestamp string with the given layout to 28 | // a unix time in seconds. 29 | func parseTime(value string, picture jtypes.OptionalString, tz jtypes.OptionalString) (int64, error) { 30 | 31 | if picture.String == "" { 32 | picture = jtypes.NewOptionalString(defaultDateFormat) 33 | } 34 | 35 | ms, err := jlib.ToMillis(value, picture, tz) 36 | if err != nil { 37 | return 0, err 38 | } 39 | 40 | return ms / 1000, nil 41 | } 42 | -------------------------------------------------------------------------------- /jsonata-server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "flag" 11 | "fmt" 12 | "log" 13 | "net/http" 14 | _ "net/http/pprof" 15 | "strings" 16 | 17 | jsonata "github.com/blues/jsonata-go" 18 | "github.com/blues/jsonata-go/jtypes" 19 | ) 20 | 21 | func init() { 22 | 23 | argUndefined0 := jtypes.ArgUndefined(0) 24 | 25 | exts := map[string]jsonata.Extension{ 26 | "formatTime": { 27 | Func: formatTime, 28 | UndefinedHandler: argUndefined0, 29 | }, 30 | "parseTime": { 31 | Func: parseTime, 32 | UndefinedHandler: argUndefined0, 33 | }, 34 | } 35 | 36 | if err := jsonata.RegisterExts(exts); err != nil { 37 | panic(err) 38 | } 39 | } 40 | 41 | func main() { 42 | 43 | port := flag.Uint("port", 8080, "The port `number` to serve on") 44 | flag.Parse() 45 | 46 | http.HandleFunc("/eval", evaluate) 47 | http.HandleFunc("/bench", benchmark) 48 | http.Handle("/", http.FileServer(http.Dir("site"))) 49 | 50 | log.Printf("Starting JSONata Server on port %d:\n", *port) 51 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)) 52 | } 53 | 54 | func evaluate(w http.ResponseWriter, r *http.Request) { 55 | 56 | input := strings.TrimSpace(r.FormValue("json")) 57 | if input == "" { 58 | http.Error(w, "Input is empty", http.StatusBadRequest) 59 | return 60 | } 61 | 62 | expression := strings.TrimSpace(r.FormValue("expr")) 63 | if expression == "" { 64 | http.Error(w, "Expression is empty", http.StatusBadRequest) 65 | return 66 | } 67 | 68 | b, status, err := eval(input, expression) 69 | if err != nil { 70 | log.Println(err) 71 | http.Error(w, err.Error(), status) 72 | return 73 | } 74 | 75 | if _, err := w.Write(b); err != nil { 76 | log.Fatal(err) 77 | } 78 | } 79 | 80 | func eval(input, expression string) (b []byte, status int, err error) { 81 | 82 | defer func() { 83 | if r := recover(); r != nil { 84 | b = nil 85 | status = http.StatusInternalServerError 86 | err = fmt.Errorf("PANIC: %v", r) 87 | return 88 | } 89 | }() 90 | 91 | // Decode the JSON. 92 | var data interface{} 93 | if err := json.Unmarshal([]byte(input), &data); err != nil { 94 | return nil, http.StatusBadRequest, fmt.Errorf("input error: %s", err) 95 | } 96 | 97 | // Compile the JSONata expression. 98 | expr, err := jsonata.Compile(expression) 99 | if err != nil { 100 | return nil, http.StatusBadRequest, fmt.Errorf("compile error: %s", err) 101 | } 102 | 103 | // Evaluate the JSONata expression. 104 | result, err := expr.Eval(data) 105 | if err != nil { 106 | if err == jsonata.ErrUndefined { 107 | // Don't treat not finding any results as an error. 108 | return []byte("No results found"), http.StatusOK, nil 109 | } 110 | return nil, http.StatusInternalServerError, fmt.Errorf("eval error: %s", err) 111 | } 112 | 113 | // Return the JSONified results. 114 | b, err = jsonify(result) 115 | if err != nil { 116 | return nil, http.StatusInternalServerError, fmt.Errorf("encode error: %s", err) 117 | } 118 | 119 | return b, http.StatusOK, nil 120 | } 121 | 122 | func jsonify(v interface{}) ([]byte, error) { 123 | 124 | b := bytes.Buffer{} 125 | e := json.NewEncoder(&b) 126 | e.SetIndent("", " ") 127 | if err := e.Encode(v); err != nil { 128 | return nil, err 129 | } 130 | 131 | return b.Bytes(), nil 132 | } 133 | -------------------------------------------------------------------------------- /jsonata-server/site/assets/css/codemirror.min.css: -------------------------------------------------------------------------------- 1 | .CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor-mark{background-color:rgba(20,255,20,.5);-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:-20px;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0} 2 | /*# sourceMappingURL=codemirror.min.css.map */ -------------------------------------------------------------------------------- /jsonata-server/site/assets/css/normalize.min.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}/*# sourceMappingURL=normalize.min.css.map */ -------------------------------------------------------------------------------- /jsonata-server/site/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 15px; 3 | font-family: Helvetica, Arial, sans-serif; 4 | color: #333; 5 | background: #eee; 6 | } 7 | 8 | header { 9 | margin: 0 2em; 10 | height: 5.5em; 11 | line-height: 5.5em; 12 | } 13 | 14 | header h1, header ul { 15 | margin: 0; 16 | } 17 | 18 | header h1 { 19 | float: left; 20 | font-size: 1.65em; 21 | } 22 | 23 | header ul { 24 | float: right; 25 | } 26 | 27 | header li { 28 | display: inline-block; 29 | padding: 0 .75em; 30 | } 31 | 32 | header li a { 33 | display: block; 34 | } 35 | 36 | a:link, a:visited { 37 | color: #3b4f5e; 38 | text-decoration: none; 39 | text-transform: uppercase; 40 | } 41 | 42 | a:active, a:hover { 43 | color:#333; 44 | } 45 | 46 | .main { 47 | position: absolute; 48 | top: 5.5em; 49 | left: 0; 50 | right: 0; 51 | bottom: 0; 52 | } 53 | 54 | /* SplitJS styles */ 55 | 56 | .split, .gutter.gutter-horizontal { 57 | height: 100%; 58 | float: left; 59 | } 60 | 61 | .gutter { 62 | background-color: inherit; 63 | background-repeat: no-repeat; 64 | background-position: 50%; 65 | } 66 | 67 | .gutter.gutter-vertical { 68 | cursor: ns-resize; 69 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII="); 70 | } 71 | 72 | .gutter.gutter-horizontal { 73 | cursor: ew-resize; 74 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=="); 75 | } 76 | 77 | /* CodeMirror styles */ 78 | 79 | .CodeMirror { 80 | font-family: 'Roboto Mono', monospace; 81 | background: #fefefe; 82 | border-radius: 2px; 83 | border: 1px solid #ccc; 84 | width: calc(100% - 2px); 85 | height: calc(100% - 2px); 86 | } 87 | 88 | textarea[readonly] + .CodeMirror { 89 | background: #f6f6f6; 90 | } 91 | -------------------------------------------------------------------------------- /jsonata-server/site/assets/js/jsonata-codemirror.js: -------------------------------------------------------------------------------- 1 | 2 | // CodeMirror syntax highlighting rules for JSONata. 3 | 4 | CodeMirror.defineMode("jsonata", function(config, parserConfig) { 5 | var templateMode = parserConfig.template; 6 | var jsonata = parserConfig.jsonata; 7 | 8 | const operators = { 9 | '.': 75, 10 | '[': 80, 11 | ']': 0, 12 | '{': 70, 13 | '}': 0, 14 | '(': 80, 15 | ')': 0, 16 | ',': 0, 17 | '@': 75, 18 | '#': 70, 19 | ';': 80, 20 | ':': 80, 21 | '?': 20, 22 | '+': 50, 23 | '-': 50, 24 | '*': 60, 25 | '/': 60, 26 | '%': 60, 27 | '|': 20, 28 | '=': 40, 29 | '<': 40, 30 | '>': 40, 31 | '`': 80, 32 | '**': 60, 33 | '..': 20, 34 | ':=': 30, 35 | '!=': 40, 36 | '<=': 40, 37 | '>=': 40, 38 | 'and': 30, 39 | 'or': 25, 40 | '||' : 50, 41 | '!': 0 // not an operator, but needed as a stop character for name tokens 42 | }; 43 | 44 | const escapes = { // JSON string escape sequences - see json.org 45 | '"': '"', 46 | '\\': '\\', 47 | '/': '/', 48 | 'b': '\b', 49 | 'f': '\f', 50 | 'n': '\n', 51 | 'r': '\r', 52 | 't': '\t' 53 | }; 54 | 55 | var tokenizer = function(path) { 56 | var position = 0; 57 | var length = path.length; 58 | 59 | var create = function(type, value) { 60 | var obj = { type: type, value: value, position: position}; 61 | return obj; 62 | }; 63 | 64 | var next = function() { 65 | if(position >= length) return null; 66 | var currentChar = path.charAt(position); 67 | // skip whitespace 68 | while(position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { 69 | position++; 70 | currentChar = path.charAt(position); 71 | } 72 | // handle double-char operators 73 | if(currentChar === '.' && path.charAt(position+1) === '.') { 74 | // double-dot .. range operator 75 | position += 2; 76 | return create('operator', '..'); 77 | } 78 | if(currentChar === '|' && path.charAt(position+1) === '|') { 79 | // double-pipe || string concatenator 80 | position += 2; 81 | return create('operator', '||'); 82 | } 83 | if(currentChar === ':' && path.charAt(position+1) === '=') { 84 | // := assignment 85 | position += 2; 86 | return create('operator', ':='); 87 | } 88 | if(currentChar === '!' && path.charAt(position+1) === '=') { 89 | // != 90 | position += 2; 91 | return create('operator', '!='); 92 | } 93 | if(currentChar === '>' && path.charAt(position+1) === '=') { 94 | // >= 95 | position += 2; 96 | return create('operator', '>='); 97 | } 98 | if(currentChar === '<' && path.charAt(position+1) === '=') { 99 | // <= 100 | position += 2; 101 | return create('operator', '<='); 102 | } 103 | if(currentChar === '*' && path.charAt(position+1) === '*') { 104 | // ** descendant wildcard 105 | position += 2; 106 | return create('operator', '**'); 107 | } 108 | // test for operators 109 | if(operators.hasOwnProperty(currentChar)) { 110 | position++; 111 | return create('operator', currentChar); 112 | } 113 | // test for string literals 114 | if(currentChar === '"' || currentChar === "'") { 115 | var quoteType = currentChar; 116 | // double quoted string literal - find end of string 117 | position++; 118 | var qstr = ""; 119 | while(position < length) { 120 | currentChar = path.charAt(position); 121 | if(currentChar === '\\') { // escape sequence 122 | position++; 123 | currentChar = path.charAt(position); 124 | if(escapes.hasOwnProperty(currentChar)) { 125 | qstr += escapes[currentChar]; 126 | } else if(currentChar === 'u') { 127 | // \u should be followed by 4 hex digits 128 | var octets = path.substr(position+1, 4); 129 | if(/^[0-9a-fA-F]+$/.test(octets)) { 130 | var codepoint = parseInt(octets, 16); 131 | qstr += String.fromCharCode(codepoint); 132 | position += 4; 133 | } else { 134 | throw new Error('The escape sequence \\u must be followed by 4 hex digits at column ' + position); 135 | } 136 | } else { 137 | // illegal escape sequence 138 | throw new Error('unsupported escape sequence: \\' + currentChar + ' at column ' + position); 139 | } 140 | } else if(currentChar === quoteType) { 141 | position++; 142 | return create('string', qstr); 143 | } else { 144 | qstr += currentChar; 145 | } 146 | position++; 147 | } 148 | throw new Error('no terminating quote found in string literal starting at column ' + position); 149 | } 150 | // test for numbers 151 | var numregex = /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?/; 152 | var match = numregex.exec(path.substring(position)); 153 | if(match !== null) { 154 | var num = parseFloat(match[0]); 155 | if(!isNaN(num) && isFinite(num)) { 156 | position += match[0].length; 157 | return create('number', num); 158 | } else { 159 | throw new Error('Number out of range: ' + match[0]); 160 | } 161 | } 162 | // test for names 163 | var i = position; 164 | var ch; 165 | var name; 166 | while(true) { 167 | ch = path.charAt(i); 168 | if(i == length || ' \t\n\r\v'.indexOf(ch) > -1 || operators.hasOwnProperty(ch)) { 169 | if(path.charAt(position) === '$') { 170 | // variable reference 171 | name = path.substring(position + 1, i); 172 | position = i; 173 | return create('variable', name); 174 | } else { 175 | name = path.substring(position, i); 176 | position = i; 177 | switch(name) { 178 | case 'and': 179 | case 'or': 180 | return create('operator', name); 181 | case 'true': 182 | return create('value', true); 183 | case 'false': 184 | return create('value', false); 185 | case 'null': 186 | return create('value', null); 187 | default: 188 | if(position == length && name === '') { 189 | // whitespace at end of input 190 | return null; 191 | } 192 | return create('name', name); 193 | } 194 | } 195 | } else { 196 | i++; 197 | } 198 | } 199 | }; 200 | 201 | return next; 202 | }; 203 | 204 | var templatizer = function(text) { 205 | var position = 0; 206 | var length = text.length; 207 | 208 | var create = function(type, value) { 209 | var obj = { type: type, value: value, position: position}; 210 | return obj; 211 | }; 212 | 213 | var next = function() { 214 | if(position >= length) return null; 215 | var currentChar = text.charAt(position); 216 | // skip whitespace 217 | while(position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { 218 | position++; 219 | currentChar = text.charAt(position); 220 | } 221 | 222 | if(currentChar === '{' && text.charAt(position+1) === '{') { 223 | // found {{ 224 | position += 2; 225 | // parse what follows using the jsonata parser 226 | var rest = text.substring(position); 227 | try { 228 | jsonata.parser(rest); 229 | // if we get here, we parsed to the end of the buffer with no closing handlebars 230 | position += rest.length; 231 | return create('variable'); 232 | } catch (err) { 233 | if (err.token === '(end)') { 234 | position = length; 235 | return create('variable'); 236 | } 237 | if (rest.charAt(err.position - 1) != "}" || rest.charAt(err.position) != "}") { 238 | // no closing handlbars 239 | position += err.position; 240 | return create('variable'); 241 | } 242 | position += err.position + 1; 243 | return create('variable'); 244 | } 245 | } else { 246 | // search forward for next {{ 247 | position = text.indexOf("{{", position); 248 | if(position != -1) { 249 | return create('operator'); 250 | } 251 | position = length; 252 | return create('operator'); 253 | } 254 | 255 | }; 256 | 257 | return next; 258 | }; 259 | 260 | var TOKEN_NAMES = { 261 | 'operator': 'operator', 262 | 'variable': 'string-2', 263 | 'string': 'string', 264 | 'number': 'number', 265 | 'value': 'keyword', 266 | 'name': 'attribute' 267 | }; 268 | 269 | var currentIndent = 0; 270 | 271 | return { 272 | token: function(stream) { 273 | var lexer; 274 | if(templateMode) { 275 | lexer = templatizer(stream.string.substr(stream.pos)); 276 | } else { 277 | lexer = tokenizer(stream.string.substr(stream.pos)); 278 | } 279 | var token; 280 | try { 281 | token = lexer(); 282 | } catch(err) { 283 | token = null; 284 | } 285 | if(token === null) { 286 | stream.skipToEnd(); 287 | return null; 288 | } 289 | var length = token.position; 290 | while(length > 0) { 291 | stream.next(); 292 | length--; 293 | } 294 | 295 | var style = TOKEN_NAMES[token.type]; 296 | return style; 297 | } 298 | }; 299 | }); 300 | -------------------------------------------------------------------------------- /jsonata-server/site/assets/js/split.min.js: -------------------------------------------------------------------------------- 1 | /*! Split.js - v1.3.5 */ 2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Split=t()}(this,function(){"use strict";var e=window,t=e.document,n="addEventListener",i="removeEventListener",r="getBoundingClientRect",s=function(){return!1},o=e.attachEvent&&!e[n],a=["","-webkit-","-moz-","-o-"].filter(function(e){var n=t.createElement("div");return n.style.cssText="width:"+e+"calc(9px)",!!n.style.length}).shift()+"calc",l=function(e){return"string"==typeof e||e instanceof String?t.querySelector(e):e};return function(u,c){function z(e,t,n){var i=A(y,t,n);Object.keys(i).forEach(function(t){return e.style[t]=i[t]})}function h(e,t){var n=B(y,t);Object.keys(n).forEach(function(t){return e.style[t]=n[t]})}function f(e){var t=E[this.a],n=E[this.b],i=t.size+n.size;t.size=e/this.size*i,n.size=i-e/this.size*i,z(t.element,t.size,this.aGutterSize),z(n.element,n.size,this.bGutterSize)}function m(e){var t;this.dragging&&((t="touches"in e?e.touches[0][b]-this.start:e[b]-this.start)<=E[this.a].minSize+M+this.aGutterSize?t=E[this.a].minSize+this.aGutterSize:t>=this.size-(E[this.b].minSize+M+this.bGutterSize)&&(t=this.size-(E[this.b].minSize+this.bGutterSize)),f.call(this,t),c.onDrag&&c.onDrag())}function g(){var e=E[this.a].element,t=E[this.b].element;this.size=e[r]()[y]+t[r]()[y]+this.aGutterSize+this.bGutterSize,this.start=e[r]()[G]}function d(){var t=this,n=E[t.a].element,r=E[t.b].element;t.dragging&&c.onDragEnd&&c.onDragEnd(),t.dragging=!1,e[i]("mouseup",t.stop),e[i]("touchend",t.stop),e[i]("touchcancel",t.stop),t.parent[i]("mousemove",t.move),t.parent[i]("touchmove",t.move),delete t.stop,delete t.move,n[i]("selectstart",s),n[i]("dragstart",s),r[i]("selectstart",s),r[i]("dragstart",s),n.style.userSelect="",n.style.webkitUserSelect="",n.style.MozUserSelect="",n.style.pointerEvents="",r.style.userSelect="",r.style.webkitUserSelect="",r.style.MozUserSelect="",r.style.pointerEvents="",t.gutter.style.cursor="",t.parent.style.cursor=""}function S(t){var i=this,r=E[i.a].element,o=E[i.b].element;!i.dragging&&c.onDragStart&&c.onDragStart(),t.preventDefault(),i.dragging=!0,i.move=m.bind(i),i.stop=d.bind(i),e[n]("mouseup",i.stop),e[n]("touchend",i.stop),e[n]("touchcancel",i.stop),i.parent[n]("mousemove",i.move),i.parent[n]("touchmove",i.move),r[n]("selectstart",s),r[n]("dragstart",s),o[n]("selectstart",s),o[n]("dragstart",s),r.style.userSelect="none",r.style.webkitUserSelect="none",r.style.MozUserSelect="none",r.style.pointerEvents="none",o.style.userSelect="none",o.style.webkitUserSelect="none",o.style.MozUserSelect="none",o.style.pointerEvents="none",i.gutter.style.cursor=j,i.parent.style.cursor=j,g.call(i)}function v(e){e.forEach(function(t,n){if(n>0){var i=F[n-1],r=E[i.a],s=E[i.b];r.size=e[n-1],s.size=t,z(r.element,r.size,i.aGutterSize),z(s.element,s.size,i.bGutterSize)}})}function p(){F.forEach(function(e){e.parent.removeChild(e.gutter),E[e.a].element.style[y]="",E[e.b].element.style[y]=""})}void 0===c&&(c={});var y,b,G,E,w=l(u[0]).parentNode,D=e.getComputedStyle(w).flexDirection,U=c.sizes||u.map(function(){return 100/u.length}),k=void 0!==c.minSize?c.minSize:100,x=Array.isArray(k)?k:u.map(function(){return k}),L=void 0!==c.gutterSize?c.gutterSize:10,M=void 0!==c.snapOffset?c.snapOffset:30,O=c.direction||"horizontal",j=c.cursor||("horizontal"===O?"ew-resize":"ns-resize"),C=c.gutter||function(e,n){var i=t.createElement("div");return i.className="gutter gutter-"+n,i},A=c.elementStyle||function(e,t,n){var i={};return"string"==typeof t||t instanceof String?i[e]=t:i[e]=o?t+"%":a+"("+t+"% - "+n+"px)",i},B=c.gutterStyle||function(e,t){return n={},n[e]=t+"px",n;var n};"horizontal"===O?(y="width","clientWidth",b="clientX",G="left","paddingLeft"):"vertical"===O&&(y="height","clientHeight",b="clientY",G="top","paddingTop");var F=[];return E=u.map(function(e,t){var i,s={element:l(e),size:U[t],minSize:x[t]};if(t>0&&(i={a:t-1,b:t,dragging:!1,isFirst:1===t,isLast:t===u.length-1,direction:O,parent:w},i.aGutterSize=L,i.bGutterSize=L,i.isFirst&&(i.aGutterSize=L/2),i.isLast&&(i.bGutterSize=L/2),"row-reverse"===D||"column-reverse"===D)){var a=i.a;i.a=i.b,i.b=a}if(!o&&t>0){var c=C(t,O);h(c,L),c[n]("mousedown",S.bind(i)),c[n]("touchstart",S.bind(i)),w.insertBefore(c,s.element),i.gutter=c}0===t||t===u.length-1?z(s.element,s.size,L/2):z(s.element,s.size,L);var f=s.element[r]()[y];return f0&&F.push(i),s}),o?{setSizes:v,destroy:p}:{setSizes:v,getSizes:function(){return E.map(function(e){return e.size})},collapse:function(e){if(e===F.length){var t=F[e-1];g.call(t),o||f.call(t,t.size-t.bGutterSize)}else{var n=F[e];g.call(n),o||f.call(n,n.aGutterSize)}},destroy:p}}}); -------------------------------------------------------------------------------- /jsonata-server/site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blues/jsonata-go/31e6058dd1f53d2ad86f85fc331cadc0631a624b/jsonata-server/site/favicon.ico -------------------------------------------------------------------------------- /jsonata-server/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSONata Server 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

JSONata Server

20 | 25 |
26 |
27 |
28 | 29 |
30 | 38 |
39 | 40 | 312 | 313 | -------------------------------------------------------------------------------- /jsonata-test/.gitignore: -------------------------------------------------------------------------------- 1 | # Executables 2 | jsonata-test 3 | -------------------------------------------------------------------------------- /jsonata-test/README.md: -------------------------------------------------------------------------------- 1 | # JSONata Test 2 | 3 | A CLI tool for running jsonata-go against the [JSONata test suite](https://github.com/jsonata-js/jsonata/tree/master/test/test-suite). 4 | 5 | ## Install 6 | 7 | go install github.com/blues/jsonata-test 8 | 9 | ## Usage 10 | 11 | 1. Clone the [jsonata-js](https://github.com/jsonata-js/jsonata) repository to your local machine. 12 | 13 | git clone https://github.com/jsonata-js/jsonata 14 | 15 | 2. To access a particular version of the test suite, check out the relevant branch, e.g. 16 | 17 | git checkout v1.8.4 18 | 19 | 3. Run the test tool, specifying the location of the JSONata `test-suite` directory in the command line, e.g. 20 | 21 | jsonata-test ~/projects/jsonata/test/test-suite 22 | 23 | ## Known issues 24 | 25 | This library was originally developed against jsonata-js 1.5 and has thus far implemented a subset of features from newer version of that library. You can see potential differences by looking at the [jsonata-js changelog](https://github.com/jsonata-js/jsonata/blob/master/CHANGELOG.md). 26 | 27 | While most tests pass from jsonata-js 1.5 do pass, currently there are **1598 tests** in the JSONata 1.8.4 test suite. Running against the 1.8.4 test suite results in **307 failing tests**. The failures are mostly related to functionality in newer versions of JSONata that this library does not yet implement. The outstanding issues are summarised below, split into the categories "Won't fix", "To be fixed" and "To be investigated". 28 | 29 | ### Won't Fix 30 | 31 | #### Regex matches on zero-length strings 32 | 33 | jsonata-js throws an error if a regular expression matches a zero length string. It does this because repeatedly calling JavaScript's [Regexp.exec](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec) method can cause an infinite loop if it matches a zero length string. Go's regex handling doesn't have this problem so there's no real need to take the precaution. 34 | 35 | ### To be investigated 36 | 37 | #### Null handling 38 | 39 | jsonata-go uses `*interface{}(nil)` to represent the JSON value null. This is a nil value but it's distinguishable from `interface{}(nil)` (non-pointer) which indicates that a value does not exist. That's useful inside JSONata but Go's json package does not make that distinction. Some tests fail because jsonata-go returns a differently-typed nil. I don't *think* this will cause any practical problems (because the returned value still equals nil) but I'd like to look into it more. 40 | 41 | #### Rounding when converting numbers to strings 42 | 43 | JSONata's `$string()` function converts objects to their JSON representation. In jsonata-js, any numbers in the object are rounded to 15 decimal places so that floating point errors are discarded. The rounding takes place in a callback function passed to JavaScript's JSON encoding function. I haven't found a way to replicate this in Go. It would be a nice feature to have though. 44 | 45 | ### To be fixed 46 | Some functions like `$formatInteger()` and `$parseInteger()` from newer versions of the jsonata-js library are not yet implemented. 47 | -------------------------------------------------------------------------------- /jsonata-test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "regexp" 13 | "strings" 14 | 15 | jsonata "github.com/blues/jsonata-go" 16 | types "github.com/blues/jsonata-go/jtypes" 17 | ) 18 | 19 | type testCase struct { 20 | Expr string 21 | ExprFile string `json:"expr-file"` 22 | Category string 23 | Data interface{} 24 | Dataset string 25 | Description string 26 | TimeLimit int 27 | Depth int 28 | Bindings map[string]interface{} 29 | Result interface{} 30 | Undefined bool 31 | Error string `json:"code"` 32 | Token string 33 | Unordered bool 34 | } 35 | 36 | func main() { 37 | var group string 38 | var verbose bool 39 | 40 | flag.BoolVar(&verbose, "verbose", false, "verbose output") 41 | flag.StringVar(&group, "group", "", "restrict to one or more test groups") 42 | flag.Parse() 43 | 44 | if flag.NArg() != 1 { 45 | fmt.Fprintln(os.Stderr, "Syntax: jsonata-test [options] ") 46 | os.Exit(1) 47 | } 48 | 49 | root := flag.Arg(0) 50 | testdir := filepath.Join(root, "groups") 51 | datadir := filepath.Join(root, "datasets") 52 | 53 | err := run(testdir, datadir, group) 54 | if err != nil { 55 | fmt.Fprintf(os.Stderr, "Error while running: %s\n", err) 56 | os.Exit(2) 57 | } 58 | 59 | fmt.Fprintln(os.Stdout, "OK") 60 | } 61 | 62 | // run runs all test cases 63 | func run(testdir string, datadir string, filter string) error { 64 | var numPassed, numFailed int 65 | err := filepath.Walk(testdir, func(path string, info os.FileInfo, walkFnErr error) error { 66 | var dirName string 67 | 68 | if info.IsDir() { 69 | if path == testdir { 70 | return nil 71 | } 72 | dirName = filepath.Base(path) 73 | if filter != "" && !strings.Contains(dirName, filter) { 74 | return filepath.SkipDir 75 | } 76 | return nil 77 | } 78 | 79 | // Ignore files with names ending with .jsonata, these 80 | // are not test cases 81 | if filepath.Ext(path) == ".jsonata" { 82 | return nil 83 | } 84 | 85 | testCases, err := loadTestCases(path) 86 | if err != nil { 87 | return fmt.Errorf("walk %s: %s", path, err) 88 | } 89 | 90 | for _, testCase := range testCases { 91 | failed, err := runTest(testCase, datadir, path) 92 | 93 | if err != nil { 94 | return err 95 | } 96 | if failed { 97 | numFailed++ 98 | } else { 99 | numPassed++ 100 | } 101 | } 102 | 103 | return nil 104 | }) 105 | 106 | if err != nil { 107 | return fmt.Errorf("walk %s: ", err) 108 | } 109 | 110 | fmt.Fprintln(os.Stdout) 111 | fmt.Fprintln(os.Stdout, numPassed, "passed", numFailed, "failed") 112 | return nil 113 | } 114 | 115 | // runTest runs a single test case 116 | func runTest(tc testCase, dataDir string, path string) (bool, error) { 117 | // Some tests assume JavaScript-style object traversal, 118 | // these are marked as unordered and can be skipped 119 | // See https://github.com/jsonata-js/jsonata/issues/179 120 | if tc.Unordered { 121 | return false, nil 122 | } 123 | 124 | if tc.TimeLimit != 0 { 125 | return false, nil 126 | } 127 | 128 | // If this test has an associated dataset, load it 129 | data := tc.Data 130 | if tc.Dataset != "" { 131 | var dest interface{} 132 | err := readJSONFile(filepath.Join(dataDir, tc.Dataset+".json"), &dest) 133 | if err != nil { 134 | return false, err 135 | } 136 | data = dest 137 | } 138 | 139 | var failed bool 140 | expr, unQuoted := replaceQuotesInPaths(tc.Expr) 141 | got, _ := eval(expr, tc.Bindings, data) 142 | 143 | if !equalResults(got, tc.Result) { 144 | failed = true 145 | printTestCase(os.Stderr, tc, strings.TrimSuffix(filepath.Base(path), ".json")) 146 | fmt.Fprintf(os.Stderr, "Test file: %s \n", path) 147 | 148 | if tc.Category != "" { 149 | fmt.Fprintf(os.Stderr, "Category: %s \n", tc.Category) 150 | } 151 | if tc.Description != "" { 152 | fmt.Fprintf(os.Stderr, "Description: %s \n", tc.Description) 153 | } 154 | 155 | fmt.Fprintf(os.Stderr, "Expression: %s\n", expr) 156 | if unQuoted { 157 | fmt.Fprintf(os.Stderr, "Unquoted: %t\n", unQuoted) 158 | } 159 | fmt.Fprintf(os.Stderr, "Expected Result: %v [%T]\n", tc.Result, tc.Result) 160 | fmt.Fprintf(os.Stderr, "Actual Result: %v [%T]\n", got, got) 161 | } 162 | 163 | // TODO this block is commented out to make staticcheck happy, 164 | // but we should check that the error is the same as the js one 165 | // var exp error 166 | // if tc.Undefined { 167 | // exp = jsonata.ErrUndefined 168 | // } else { 169 | // exp = convertError(tc.Error) 170 | // } 171 | 172 | // if !reflect.DeepEqual(err, exp) { 173 | // TODO: Compare actual/expected errors 174 | // } 175 | 176 | return failed, nil 177 | } 178 | 179 | // loadTestExprFile loads a jsonata expression from a file and returns the 180 | // expression 181 | // For example, one test looks like this 182 | // { 183 | // "expr-file": "case000.jsonata", 184 | // "dataset": null, 185 | // "bindings": {}, 186 | // "result": 2 187 | // } 188 | // 189 | // We want to load the expression from case000.jsonata so we can use it 190 | // as an expression in the test case 191 | func loadTestExprFile(testPath string, exprFileName string) (string, error) { 192 | splitPath := strings.Split(testPath, "/") 193 | splitPath[len(splitPath)-1] = exprFileName 194 | exprFilePath := strings.Join(splitPath, "/") 195 | 196 | content, err := ioutil.ReadFile(exprFilePath) 197 | if err != nil { 198 | return "", err 199 | } 200 | 201 | return string(content), nil 202 | } 203 | 204 | // loadTestCases loads all of the json data for tests and converts them to test cases 205 | func loadTestCases(path string) ([]testCase, error) { 206 | // Test cases are contained in json files. They consist of either 207 | // one test case in the file or an array of test cases. 208 | // Since we don't know which it will be until we load the file, 209 | // first try to demarshall it a single case, and if there is an 210 | // error, try again demarshalling it into an array of test cases 211 | var tc testCase 212 | err := readJSONFile(path, &tc) 213 | if err != nil { 214 | var tcs []testCase 215 | err := readJSONFile(path, &tcs) 216 | if err != nil { 217 | return nil, err 218 | 219 | } 220 | 221 | // If any of the tests specify an expression file, load it from 222 | // disk and add it to the test case 223 | for _, testCase := range tcs { 224 | if testCase.ExprFile != "" { 225 | expr, err := loadTestExprFile(path, testCase.ExprFile) 226 | if err != nil { 227 | return nil, err 228 | } 229 | testCase.Expr = expr 230 | } 231 | } 232 | return tcs, nil 233 | } 234 | 235 | // If we have gotten here then there was only one test specified in the 236 | // tests file. 237 | 238 | // If the test specifies an expression file, load it from 239 | // disk and add it to the test case 240 | if tc.ExprFile != "" { 241 | expr, err := loadTestExprFile(path, tc.ExprFile) 242 | if err != nil { 243 | return nil, err 244 | } 245 | tc.Expr = expr 246 | } 247 | 248 | return []testCase{tc}, nil 249 | } 250 | 251 | func printTestCase(w io.Writer, tc testCase, name string) { 252 | fmt.Fprintln(w) 253 | fmt.Fprintf(w, "Failed Test Case: %s\n", name) 254 | switch { 255 | case tc.Data != nil: 256 | fmt.Fprintf(w, "Data: %v\n", tc.Data) 257 | case tc.Dataset != "": 258 | fmt.Fprintf(w, "Dataset: %s\n", tc.Dataset) 259 | default: 260 | fmt.Fprintln(w, "Data: N/A") 261 | } 262 | if tc.Error != "" { 263 | fmt.Fprintf(w, "Expected error code: %v\n", tc.Error) 264 | } 265 | if len(tc.Bindings) > 0 { 266 | fmt.Fprintf(w, "Bindings: %v\n", tc.Bindings) 267 | } 268 | } 269 | 270 | func eval(expression string, bindings map[string]interface{}, data interface{}) (interface{}, error) { 271 | expr, err := jsonata.Compile(expression) 272 | if err != nil { 273 | return nil, err 274 | } 275 | 276 | err = expr.RegisterVars(bindings) 277 | if err != nil { 278 | return nil, err 279 | } 280 | 281 | return expr.Eval(data) 282 | } 283 | 284 | func equalResults(x, y interface{}) bool { 285 | if reflect.DeepEqual(x, y) { 286 | return true 287 | } 288 | 289 | vx := types.Resolve(reflect.ValueOf(x)) 290 | vy := types.Resolve(reflect.ValueOf(y)) 291 | 292 | if types.IsArray(vx) && types.IsArray(vy) { 293 | if vx.Len() != vy.Len() { 294 | return false 295 | } 296 | for i := 0; i < vx.Len(); i++ { 297 | if !equalResults(vx.Index(i).Interface(), vy.Index(i).Interface()) { 298 | return false 299 | } 300 | } 301 | return true 302 | } 303 | 304 | ix, okx := types.AsNumber(vx) 305 | iy, oky := types.AsNumber(vy) 306 | if okx && oky && ix == iy { 307 | return true 308 | } 309 | 310 | sx, okx := types.AsString(vx) 311 | sy, oky := types.AsString(vy) 312 | if okx && oky && sx == sy { 313 | return true 314 | } 315 | 316 | bx, okx := types.AsBool(vx) 317 | by, oky := types.AsBool(vy) 318 | if okx && oky && bx == by { 319 | return true 320 | } 321 | 322 | return false 323 | } 324 | 325 | func readJSONFile(path string, dest interface{}) error { 326 | b, err := ioutil.ReadFile(path) 327 | if err != nil { 328 | return fmt.Errorf("ReadFile %s: %s", path, err) 329 | } 330 | 331 | err = json.Unmarshal(b, dest) 332 | if err != nil { 333 | return fmt.Errorf("unmarshal %s: %s", path, err) 334 | } 335 | 336 | return nil 337 | } 338 | 339 | var ( 340 | reQuotedPath = regexp.MustCompile(`([A-Za-z\$\\*\` + "`" + `])\.[\"']([ \.0-9A-Za-z]+?)[\"']`) 341 | reQuotedPathStart = regexp.MustCompile(`^[\"']([ \.0-9A-Za-z]+?)[\"']\.([A-Za-z\$\*\"\'])`) 342 | ) 343 | 344 | func replaceQuotesInPaths(s string) (string, bool) { 345 | var changed bool 346 | 347 | if reQuotedPathStart.MatchString(s) { 348 | s = reQuotedPathStart.ReplaceAllString(s, "`$1`.$2") 349 | changed = true 350 | } 351 | 352 | for reQuotedPath.MatchString(s) { 353 | s = reQuotedPath.ReplaceAllString(s, "$1.`$2`") 354 | changed = true 355 | } 356 | 357 | return s, changed 358 | } 359 | -------------------------------------------------------------------------------- /jsonata-test/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestReplaceQuotesInPaths(t *testing.T) { 6 | 7 | inputs := []string{ 8 | `[Address, Other."Alternative.Address"].City`, 9 | `Account.( $AccName := function() { $."Account Name" }; Order[OrderID = "order104"].Product{ "Account": $AccName(), "SKU-" & $string(ProductID): $."Product Name" } )`, 10 | `Account.Order.Product."Product Name".$uppercase().$substringBefore(" ")`, 11 | `"foo".**.fud`, 12 | `foo.**."fud"`, 13 | `"foo".**."fud"`, 14 | `Account.Order.Product[$."Product Name" ~> /hat/i].ProductID`, 15 | `$sort(Account.Order.Product."Product Name")`, 16 | `Account.Order.Product ~> $map(λ($prod, $index) { $index+1 & ": " & $prod."Product Name" })`, 17 | `Account.Order.Product ~> $map(λ($prod, $index, $arr) { $index+1 & "/" & $count($arr) & ": " & $prod."Product Name" })`, 18 | `Account.Order{OrderID: Product."Product Name"}`, 19 | `Account.Order.{OrderID: Product."Product Name"}`, 20 | `Account.Order.Product{$."Product Name": Price, $."Product Name": Price}`, 21 | `Account.Order{ OrderID: { "TotalPrice":$sum(Product.(Price * Quantity)), "Items": Product."Product Name" }}`, 22 | `{ "Order": Account.Order.{ "ID": OrderID, "Product": Product.{ "Name": $."Product Name", "SKU": ProductID, "Details": { "Weight": Description.Weight, "Dimensions": Description.(Width & " x " & Height & " x " & Depth) } }, "Total Price": $sum(Product.(Price * Quantity)) }}`, 23 | `Account.Order.Product[$contains($."Product Name", /hat/)].ProductID`, 24 | `Account.Order.Product[$contains($."Product Name", /hat/i)].ProductID`, 25 | `Account.Order.Product.$replace($."Product Name", /hat/i, function($match) { "foo" })`, 26 | `Account.Order.Product.$replace($."Product Name", /(h)(at)/i, function($match) { $uppercase($match.match) })`, 27 | `$.'7a'`, 28 | `$.'7'`, 29 | `$lowercase($."NI.Number")`, 30 | `$lowercase("COMPENSATION IS : " & Employment."Executive.Compensation")`, 31 | `Account[$$.Account."Account Name" = "Firefly"].*[OrderID="order104"].Product.Price`, 32 | } 33 | 34 | outputs := []string{ 35 | "[Address, Other.`Alternative.Address`].City", 36 | "Account.( $AccName := function() { $.`Account Name` }; Order[OrderID = \"order104\"].Product{ \"Account\": $AccName(), \"SKU-\" & $string(ProductID): $.`Product Name` } )", 37 | "Account.Order.Product.`Product Name`.$uppercase().$substringBefore(\" \")", 38 | "`foo`.**.fud", 39 | "foo.**.`fud`", 40 | "`foo`.**.`fud`", 41 | "Account.Order.Product[$.`Product Name` ~> /hat/i].ProductID", 42 | "$sort(Account.Order.Product.`Product Name`)", 43 | "Account.Order.Product ~> $map(λ($prod, $index) { $index+1 & \": \" & $prod.`Product Name` })", 44 | "Account.Order.Product ~> $map(λ($prod, $index, $arr) { $index+1 & \"/\" & $count($arr) & \": \" & $prod.`Product Name` })", 45 | "Account.Order{OrderID: Product.`Product Name`}", 46 | "Account.Order.{OrderID: Product.`Product Name`}", 47 | "Account.Order.Product{$.`Product Name`: Price, $.`Product Name`: Price}", 48 | "Account.Order{ OrderID: { \"TotalPrice\":$sum(Product.(Price * Quantity)), \"Items\": Product.`Product Name` }}", 49 | "{ \"Order\": Account.Order.{ \"ID\": OrderID, \"Product\": Product.{ \"Name\": $.`Product Name`, \"SKU\": ProductID, \"Details\": { \"Weight\": Description.Weight, \"Dimensions\": Description.(Width & \" x \" & Height & \" x \" & Depth) } }, \"Total Price\": $sum(Product.(Price * Quantity)) }}", 50 | "Account.Order.Product[$contains($.`Product Name`, /hat/)].ProductID", 51 | "Account.Order.Product[$contains($.`Product Name`, /hat/i)].ProductID", 52 | "Account.Order.Product.$replace($.`Product Name`, /hat/i, function($match) { \"foo\" })", 53 | "Account.Order.Product.$replace($.`Product Name`, /(h)(at)/i, function($match) { $uppercase($match.match) })", 54 | "$.`7a`", 55 | "$.`7`", 56 | "$lowercase($.`NI.Number`)", 57 | "$lowercase(\"COMPENSATION IS : \" & Employment.`Executive.Compensation`)", 58 | "Account[$$.Account.`Account Name` = \"Firefly\"].*[OrderID=\"order104\"].Product.Price", 59 | } 60 | 61 | for i := range inputs { 62 | 63 | got, ok := replaceQuotesInPaths(inputs[i]) 64 | if got != outputs[i] { 65 | t.Errorf("\n Input: %s\nExp. Output: %s\nAct. Output: %s", inputs[i], outputs[i], got) 66 | } 67 | if !ok { 68 | t.Errorf("%s: Expected true, got %t", inputs[i], ok) 69 | } 70 | } 71 | } 72 | 73 | func TestReplaceQuotesInPathsNoOp(t *testing.T) { 74 | 75 | inputs := []string{ 76 | `42 ~> "hello"`, 77 | `"john@example.com" ~> $substringAfter("@") ~> $substringBefore(".")`, 78 | `$ ~> |Account.Order.Product|{"Total":Price*Quantity},["Description", "SKU"]|`, 79 | `$ ~> |(Account.Order.Product)[0]|{"Description":"blah"}|`, 80 | } 81 | 82 | for i := range inputs { 83 | 84 | got, ok := replaceQuotesInPaths(inputs[i]) 85 | if got != inputs[i] { 86 | t.Errorf("\n Input: %s\nExp. Output: %s\nAct. Output: %s", inputs[i], inputs[i], got) 87 | } 88 | if ok { 89 | t.Errorf("%s: Expected false, got %t", inputs[i], ok) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /jsonata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | package jsonata 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "reflect" 11 | "sync" 12 | "time" 13 | "unicode" 14 | 15 | "github.com/blues/jsonata-go/jlib" 16 | "github.com/blues/jsonata-go/jparse" 17 | "github.com/blues/jsonata-go/jtypes" 18 | ) 19 | 20 | var ( 21 | globalRegistryMutex sync.RWMutex 22 | globalRegistry map[string]reflect.Value 23 | ) 24 | 25 | // An Extension describes custom functionality added to a 26 | // JSONata expression. 27 | type Extension struct { 28 | 29 | // Func is a Go function that implements the custom 30 | // functionality and returns either one or two values. 31 | // The second return value, if provided, must be an 32 | // error. 33 | Func interface{} 34 | 35 | // UndefinedHandler is a function that determines how 36 | // this extension handles undefined arguments. If 37 | // UndefinedHandler is non-nil, it is called before 38 | // Func with the same arguments. If the handler returns 39 | // true, Func is not called and undefined is returned 40 | // instead. 41 | UndefinedHandler jtypes.ArgHandler 42 | 43 | // EvalContextHandler is a function that determines how 44 | // this extension handles missing arguments. If 45 | // EvalContextHandler is non-nil, it is called before 46 | // Func with the same arguments. If the handler returns 47 | // true, the evaluation context is inserted as the first 48 | // argument when Func is called. 49 | EvalContextHandler jtypes.ArgHandler 50 | } 51 | 52 | // RegisterExts registers custom functions for use in JSONata 53 | // expressions. It is designed to be called once on program 54 | // startup (e.g. from an init function). 55 | // 56 | // Custom functions registered at the package level will be 57 | // available to all Expr objects. To register custom functions 58 | // with specific Expr objects, use the RegisterExts method. 59 | func RegisterExts(exts map[string]Extension) error { 60 | 61 | values, err := processExts(exts) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | updateGlobalRegistry(values) 67 | return nil 68 | } 69 | 70 | // RegisterVars registers custom variables for use in JSONata 71 | // expressions. It is designed to be called once on program 72 | // startup (e.g. from an init function). 73 | // 74 | // Custom variables registered at the package level will be 75 | // available to all Expr objects. To register custom variables 76 | // with specific Expr objects, use the RegisterVars method. 77 | func RegisterVars(vars map[string]interface{}) error { 78 | 79 | values, err := processVars(vars) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | updateGlobalRegistry(values) 85 | return nil 86 | } 87 | 88 | // An Expr represents a JSONata expression. 89 | type Expr struct { 90 | node jparse.Node 91 | registry map[string]reflect.Value 92 | } 93 | 94 | // Compile parses a JSONata expression and returns an Expr 95 | // that can be evaluated against JSON data. If the input is 96 | // not a valid JSONata expression, Compile returns an error 97 | // of type jparse.Error. 98 | func Compile(expr string) (*Expr, error) { 99 | 100 | node, err := jparse.Parse(expr) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | e := &Expr{ 106 | node: node, 107 | } 108 | 109 | globalRegistryMutex.RLock() 110 | e.updateRegistry(globalRegistry) 111 | globalRegistryMutex.RUnlock() 112 | 113 | return e, nil 114 | } 115 | 116 | // MustCompile is like Compile except it panics if given an 117 | // invalid expression. 118 | func MustCompile(expr string) *Expr { 119 | 120 | e, err := Compile(expr) 121 | if err != nil { 122 | panicf("could not compile %s: %s", expr, err) 123 | } 124 | 125 | return e 126 | } 127 | 128 | // Eval executes a JSONata expression against the given data 129 | // source. The input is typically the result of unmarshaling 130 | // a JSON string. The output is an object suitable for 131 | // marshaling into a JSON string. Use EvalBytes to skip the 132 | // unmarshal/marshal steps and work solely with JSON strings. 133 | // 134 | // Eval can be called multiple times, with different input 135 | // data if required. 136 | func (e *Expr) Eval(data interface{}) (interface{}, error) { 137 | input, ok := data.(reflect.Value) 138 | if !ok { 139 | input = reflect.ValueOf(data) 140 | } 141 | 142 | result, err := eval(e.node, input, e.newEnv(input)) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | if !result.IsValid() { 148 | return nil, ErrUndefined 149 | } 150 | 151 | if !result.CanInterface() { 152 | return nil, fmt.Errorf("Eval returned a non-interface value") 153 | } 154 | 155 | if result.Kind() == reflect.Ptr && result.IsNil() { 156 | return nil, nil 157 | } 158 | 159 | return result.Interface(), nil 160 | } 161 | 162 | // EvalBytes is like Eval but it accepts and returns byte slices 163 | // instead of objects. 164 | func (e *Expr) EvalBytes(data []byte) ([]byte, error) { 165 | 166 | var v interface{} 167 | 168 | err := json.Unmarshal(data, &v) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | v, err = e.Eval(v) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | return json.Marshal(v) 179 | } 180 | 181 | // RegisterExts registers custom functions for use during 182 | // evaluation. Custom functions registered with this method 183 | // are only available to this Expr object. To make custom 184 | // functions available to all Expr objects, use the package 185 | // level RegisterExts function. 186 | func (e *Expr) RegisterExts(exts map[string]Extension) error { 187 | 188 | values, err := processExts(exts) 189 | if err != nil { 190 | return err 191 | } 192 | 193 | e.updateRegistry(values) 194 | return nil 195 | } 196 | 197 | // RegisterVars registers custom variables for use during 198 | // evaluation. Custom variables registered with this method 199 | // are only available to this Expr object. To make custom 200 | // variables available to all Expr objects, use the package 201 | // level RegisterVars function. 202 | func (e *Expr) RegisterVars(vars map[string]interface{}) error { 203 | 204 | values, err := processVars(vars) 205 | if err != nil { 206 | return err 207 | } 208 | 209 | e.updateRegistry(values) 210 | return nil 211 | } 212 | 213 | // String returns a string representation of an Expr. 214 | func (e *Expr) String() string { 215 | if e.node == nil { 216 | return "" 217 | } 218 | return e.node.String() 219 | } 220 | 221 | func (e *Expr) updateRegistry(values map[string]reflect.Value) { 222 | 223 | for name, v := range values { 224 | if e.registry == nil { 225 | e.registry = make(map[string]reflect.Value, len(values)) 226 | } 227 | e.registry[name] = v 228 | } 229 | } 230 | 231 | func (e *Expr) newEnv(input reflect.Value) *environment { 232 | 233 | tc := timeCallables(time.Now()) 234 | 235 | env := newEnvironment(baseEnv, len(tc)+len(e.registry)+1) 236 | 237 | env.bind("$", input) 238 | env.bindAll(tc) 239 | env.bindAll(e.registry) 240 | 241 | return env 242 | } 243 | 244 | var ( 245 | milisT = mustGoCallable("millis", Extension{ 246 | Func: func(millis int64) int64 { 247 | return millis 248 | }, 249 | }) 250 | 251 | nowT = mustGoCallable("now", Extension{ 252 | Func: func(millis int64, picture jtypes.OptionalString, tz jtypes.OptionalString) (string, error) { 253 | return jlib.FromMillis(millis, picture, tz) 254 | }, 255 | }) 256 | ) 257 | 258 | func timeCallables(t time.Time) map[string]reflect.Value { 259 | 260 | ms := t.UnixNano() / int64(time.Millisecond) 261 | 262 | millis := &partialCallable{ 263 | callableName: callableName{ 264 | name: "millis", 265 | }, 266 | fn: milisT, 267 | args: []jparse.Node{ 268 | &jparse.NumberNode{ 269 | Value: float64(ms), 270 | }, 271 | }, 272 | } 273 | 274 | now := &partialCallable{ 275 | callableName: callableName{ 276 | name: "now", 277 | }, 278 | fn: nowT, 279 | args: []jparse.Node{ 280 | &jparse.NumberNode{ 281 | Value: float64(ms), 282 | }, 283 | &jparse.PlaceholderNode{}, 284 | &jparse.PlaceholderNode{}, 285 | }, 286 | } 287 | 288 | return map[string]reflect.Value{ 289 | "millis": reflect.ValueOf(millis), 290 | "now": reflect.ValueOf(now), 291 | } 292 | } 293 | 294 | func processExts(exts map[string]Extension) (map[string]reflect.Value, error) { 295 | 296 | var m map[string]reflect.Value 297 | 298 | for name, ext := range exts { 299 | 300 | if !validName(name) { 301 | return nil, fmt.Errorf("%s is not a valid name", name) 302 | } 303 | 304 | callable, err := newGoCallable(name, ext) 305 | if err != nil { 306 | return nil, fmt.Errorf("%s is not a valid function: %s", name, err) 307 | } 308 | 309 | if m == nil { 310 | m = make(map[string]reflect.Value, len(exts)) 311 | } 312 | m[name] = reflect.ValueOf(callable) 313 | } 314 | 315 | return m, nil 316 | } 317 | 318 | func processVars(vars map[string]interface{}) (map[string]reflect.Value, error) { 319 | 320 | var m map[string]reflect.Value 321 | 322 | for name, value := range vars { 323 | 324 | if !validName(name) { 325 | return nil, fmt.Errorf("%s is not a valid name", name) 326 | } 327 | 328 | if !validVar(value) { 329 | return nil, fmt.Errorf("%s is not a valid variable", name) 330 | } 331 | 332 | if m == nil { 333 | m = make(map[string]reflect.Value, len(vars)) 334 | } 335 | m[name] = reflect.ValueOf(value) 336 | } 337 | 338 | return m, nil 339 | } 340 | 341 | func updateGlobalRegistry(values map[string]reflect.Value) { 342 | 343 | globalRegistryMutex.Lock() 344 | 345 | for name, v := range values { 346 | if globalRegistry == nil { 347 | globalRegistry = make(map[string]reflect.Value, len(values)) 348 | } 349 | globalRegistry[name] = v 350 | } 351 | 352 | globalRegistryMutex.Unlock() 353 | } 354 | 355 | func validName(s string) bool { 356 | 357 | if len(s) == 0 { 358 | return false 359 | } 360 | 361 | for _, r := range s { 362 | if !isLetter(r) && !isDigit(r) && r != '_' { 363 | return false 364 | } 365 | } 366 | 367 | return true 368 | } 369 | 370 | func validVar(v interface{}) bool { 371 | // TODO: Variable validation. 372 | return true 373 | } 374 | 375 | func isLetter(r rune) bool { 376 | return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || unicode.IsLetter(r) 377 | } 378 | 379 | func isDigit(r rune) bool { 380 | return (r >= '0' && r <= '9') || unicode.IsDigit(r) 381 | } 382 | -------------------------------------------------------------------------------- /jtypes/funcs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | // Package jtypes (golint) 6 | package jtypes 7 | 8 | import ( 9 | "reflect" 10 | ) 11 | 12 | // Resolve (golint) 13 | func Resolve(v reflect.Value) reflect.Value { 14 | for { 15 | switch v.Kind() { 16 | case reflect.Interface, reflect.Ptr: 17 | if !v.IsNil() { 18 | v = v.Elem() 19 | break 20 | } 21 | fallthrough 22 | default: 23 | return v 24 | } 25 | } 26 | } 27 | 28 | // IsBool (golint) 29 | func IsBool(v reflect.Value) bool { 30 | return v.Kind() == reflect.Bool || resolvedKind(v) == reflect.Bool 31 | } 32 | 33 | // IsString (golint) 34 | func IsString(v reflect.Value) bool { 35 | return v.Kind() == reflect.String || resolvedKind(v) == reflect.String 36 | } 37 | 38 | // IsNumber (golint) 39 | func IsNumber(v reflect.Value) bool { 40 | return isFloat(v) || isInt(v) || isUint(v) 41 | } 42 | 43 | // IsCallable (golint) 44 | func IsCallable(v reflect.Value) bool { 45 | v = Resolve(v) 46 | return v.IsValid() && 47 | (v.Type().Implements(TypeCallable) || reflect.PtrTo(v.Type()).Implements(TypeCallable)) 48 | } 49 | 50 | // IsArray (golint) 51 | func IsArray(v reflect.Value) bool { 52 | return isArrayKind(v.Kind()) || isArrayKind(resolvedKind(v)) 53 | } 54 | 55 | func isArrayKind(k reflect.Kind) bool { 56 | return k == reflect.Slice || k == reflect.Array 57 | } 58 | 59 | // IsArrayOf (golint) 60 | func IsArrayOf(v reflect.Value, hasType func(reflect.Value) bool) bool { 61 | if !IsArray(v) { 62 | return false 63 | } 64 | 65 | v = Resolve(v) 66 | for i := 0; i < v.Len(); i++ { 67 | if !hasType(v.Index(i)) { 68 | return false 69 | } 70 | } 71 | 72 | return true 73 | } 74 | 75 | // IsMap (golint) 76 | func IsMap(v reflect.Value) bool { 77 | return resolvedKind(v) == reflect.Map 78 | } 79 | 80 | // IsStruct (golint) 81 | func IsStruct(v reflect.Value) bool { 82 | return resolvedKind(v) == reflect.Struct 83 | } 84 | 85 | // AsBool (golint) 86 | func AsBool(v reflect.Value) (bool, bool) { 87 | v = Resolve(v) 88 | 89 | switch { 90 | case IsBool(v): 91 | return v.Bool(), true 92 | default: 93 | return false, false 94 | } 95 | } 96 | 97 | // AsString (golint) 98 | func AsString(v reflect.Value) (string, bool) { 99 | v = Resolve(v) 100 | 101 | switch { 102 | case IsString(v): 103 | return v.String(), true 104 | default: 105 | return "", false 106 | } 107 | } 108 | 109 | // AsNumber (golint) 110 | func AsNumber(v reflect.Value) (float64, bool) { 111 | v = Resolve(v) 112 | 113 | switch { 114 | case isFloat(v): 115 | return v.Float(), true 116 | case isInt(v), isUint(v): 117 | return v.Convert(typeFloat64).Float(), true 118 | default: 119 | return 0, false 120 | } 121 | } 122 | 123 | // AsCallable (golint) 124 | func AsCallable(v reflect.Value) (Callable, bool) { 125 | v = Resolve(v) 126 | 127 | if v.IsValid() && v.Type().Implements(TypeCallable) && v.CanInterface() { 128 | return v.Interface().(Callable), true 129 | } 130 | 131 | if v.IsValid() && reflect.PtrTo(v.Type()).Implements(TypeCallable) && v.CanAddr() && v.Addr().CanInterface() { 132 | return v.Addr().Interface().(Callable), true 133 | } 134 | 135 | return nil, false 136 | } 137 | 138 | func isInt(v reflect.Value) bool { 139 | return isIntKind(v.Kind()) || isIntKind(resolvedKind(v)) 140 | } 141 | 142 | func isIntKind(k reflect.Kind) bool { 143 | switch k { 144 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 145 | return true 146 | default: 147 | return false 148 | } 149 | } 150 | 151 | func isUint(v reflect.Value) bool { 152 | return isUintKind(v.Kind()) || isUintKind(resolvedKind(v)) 153 | } 154 | 155 | func isUintKind(k reflect.Kind) bool { 156 | switch k { 157 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 158 | return true 159 | default: 160 | return false 161 | } 162 | } 163 | 164 | func isFloat(v reflect.Value) bool { 165 | return isFloatKind(v.Kind()) || isFloatKind(resolvedKind(v)) 166 | } 167 | 168 | func isFloatKind(k reflect.Kind) bool { 169 | switch k { 170 | case reflect.Float32, reflect.Float64: 171 | return true 172 | default: 173 | return false 174 | } 175 | } 176 | 177 | func resolvedKind(v reflect.Value) reflect.Kind { 178 | return Resolve(v).Kind() 179 | } 180 | -------------------------------------------------------------------------------- /jtypes/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Blues Inc. All rights reserved. 2 | // Use of this source code is governed by licenses granted by the 3 | // copyright holder including that found in the LICENSE file. 4 | 5 | // Package jtypes provides types and utilities for third party 6 | // extension functions. 7 | package jtypes 8 | 9 | import ( 10 | "errors" 11 | "reflect" 12 | ) 13 | 14 | var undefined reflect.Value 15 | 16 | var ( 17 | typeBool = reflect.TypeOf((*bool)(nil)).Elem() 18 | typeInt = reflect.TypeOf((*int)(nil)).Elem() 19 | typeFloat64 = reflect.TypeOf((*float64)(nil)).Elem() 20 | typeString = reflect.TypeOf((*string)(nil)).Elem() 21 | 22 | // TypeOptional (golint) 23 | TypeOptional = reflect.TypeOf((*Optional)(nil)).Elem() 24 | // TypeCallable (golint) 25 | TypeCallable = reflect.TypeOf((*Callable)(nil)).Elem() 26 | // TypeConvertible (golint) 27 | TypeConvertible = reflect.TypeOf((*Convertible)(nil)).Elem() 28 | // TypeVariant (golint) 29 | TypeVariant = reflect.TypeOf((*Variant)(nil)).Elem() 30 | // TypeValue (golint) 31 | TypeValue = reflect.TypeOf((*reflect.Value)(nil)).Elem() 32 | // TypeInterface (golint) 33 | TypeInterface = reflect.TypeOf((*interface{})(nil)).Elem() 34 | ) 35 | 36 | // ErrUndefined (golint) 37 | var ErrUndefined = errors.New("undefined") 38 | 39 | // Variant (golint) 40 | type Variant interface { 41 | ValidTypes() []reflect.Type 42 | } 43 | 44 | // Callable (golint) 45 | type Callable interface { 46 | Name() string 47 | ParamCount() int 48 | Call([]reflect.Value) (reflect.Value, error) 49 | } 50 | 51 | // Convertible (golint) 52 | type Convertible interface { 53 | ConvertTo(reflect.Type) (reflect.Value, bool) 54 | } 55 | 56 | // Optional (golint) 57 | type Optional interface { 58 | IsSet() bool 59 | Set(reflect.Value) 60 | Type() reflect.Type 61 | } 62 | 63 | type isSet bool 64 | 65 | // IsSet (golint) 66 | func (opt *isSet) IsSet() bool { 67 | return bool(*opt) 68 | } 69 | 70 | // OptionalBool (golint) 71 | type OptionalBool struct { 72 | isSet 73 | Bool bool 74 | } 75 | 76 | // NewOptionalBool (golint) 77 | func NewOptionalBool(value bool) OptionalBool { 78 | opt := OptionalBool{} 79 | opt.Set(reflect.ValueOf(value)) 80 | return opt 81 | } 82 | 83 | // Set (golint) 84 | func (opt *OptionalBool) Set(v reflect.Value) { 85 | opt.isSet = true 86 | opt.Bool = v.Bool() 87 | } 88 | 89 | // Type (golint) 90 | func (opt *OptionalBool) Type() reflect.Type { 91 | return typeBool 92 | } 93 | 94 | // OptionalInt (golint) 95 | type OptionalInt struct { 96 | isSet 97 | Int int 98 | } 99 | 100 | // NewOptionalInt (golint) 101 | func NewOptionalInt(value int) OptionalInt { 102 | opt := OptionalInt{} 103 | opt.Set(reflect.ValueOf(value)) 104 | return opt 105 | } 106 | 107 | // Set (golint) 108 | func (opt *OptionalInt) Set(v reflect.Value) { 109 | opt.isSet = true 110 | opt.Int = int(v.Int()) 111 | } 112 | 113 | // Type (golint) 114 | func (opt *OptionalInt) Type() reflect.Type { 115 | return typeInt 116 | } 117 | 118 | // OptionalFloat64 (golint) 119 | type OptionalFloat64 struct { 120 | isSet 121 | Float64 float64 122 | } 123 | 124 | // NewOptionalFloat64 (golint) 125 | func NewOptionalFloat64(value float64) OptionalFloat64 { 126 | opt := OptionalFloat64{} 127 | opt.Set(reflect.ValueOf(value)) 128 | return opt 129 | } 130 | 131 | // Set (golint) 132 | func (opt *OptionalFloat64) Set(v reflect.Value) { 133 | opt.isSet = true 134 | opt.Float64 = v.Float() 135 | } 136 | 137 | // Type (golint) 138 | func (opt *OptionalFloat64) Type() reflect.Type { 139 | return typeFloat64 140 | } 141 | 142 | // OptionalString (golint) 143 | type OptionalString struct { 144 | isSet 145 | String string 146 | } 147 | 148 | // NewOptionalString (golint) 149 | func NewOptionalString(value string) OptionalString { 150 | opt := OptionalString{} 151 | opt.Set(reflect.ValueOf(value)) 152 | return opt 153 | } 154 | 155 | // Set (golint) 156 | func (opt *OptionalString) Set(v reflect.Value) { 157 | opt.isSet = true 158 | opt.String = v.String() 159 | } 160 | 161 | // Type (golint) 162 | func (opt *OptionalString) Type() reflect.Type { 163 | return typeString 164 | } 165 | 166 | // OptionalInterface (golint) 167 | type OptionalInterface struct { 168 | isSet 169 | Interface interface{} 170 | } 171 | 172 | // NewOptionalInterface (golint) 173 | func NewOptionalInterface(value interface{}) OptionalInterface { 174 | opt := OptionalInterface{} 175 | opt.Set(reflect.ValueOf(value)) 176 | return opt 177 | } 178 | 179 | // Set (golint) 180 | func (opt *OptionalInterface) Set(v reflect.Value) { 181 | opt.isSet = true 182 | opt.Interface = v.Interface() 183 | } 184 | 185 | // Type (golint) 186 | func (opt *OptionalInterface) Type() reflect.Type { 187 | return TypeInterface 188 | } 189 | 190 | // OptionalValue (golint) 191 | type OptionalValue struct { 192 | isSet 193 | Value reflect.Value 194 | } 195 | 196 | // NewOptionalValue (golint) 197 | func NewOptionalValue(value reflect.Value) OptionalValue { 198 | opt := OptionalValue{} 199 | opt.Set(reflect.ValueOf(value)) 200 | return opt 201 | } 202 | 203 | // Set (golint) 204 | func (opt *OptionalValue) Set(v reflect.Value) { 205 | opt.isSet = true 206 | opt.Value = v.Interface().(reflect.Value) 207 | } 208 | 209 | // Type (golint) 210 | func (opt *OptionalValue) Type() reflect.Type { 211 | return TypeValue 212 | } 213 | 214 | // OptionalCallable (golint) 215 | type OptionalCallable struct { 216 | isSet 217 | Callable Callable 218 | } 219 | 220 | // NewOptionalCallable (golint) 221 | func NewOptionalCallable(value Callable) OptionalCallable { 222 | opt := OptionalCallable{} 223 | opt.Set(reflect.ValueOf(value)) 224 | return opt 225 | } 226 | 227 | // Set (golint) 228 | func (opt *OptionalCallable) Set(v reflect.Value) { 229 | opt.isSet = true 230 | opt.Callable = v.Interface().(Callable) 231 | } 232 | 233 | // Type (golint) 234 | func (opt *OptionalCallable) Type() reflect.Type { 235 | return TypeCallable 236 | } 237 | 238 | // ArgHandler (golint) 239 | type ArgHandler func([]reflect.Value) bool 240 | 241 | // ArgCountEquals (golint) 242 | func ArgCountEquals(n int) ArgHandler { 243 | return func(argv []reflect.Value) bool { 244 | return len(argv) == n 245 | } 246 | } 247 | 248 | // ArgUndefined (golint) 249 | func ArgUndefined(i int) ArgHandler { 250 | return func(argv []reflect.Value) bool { 251 | return len(argv) > i && argv[i] == undefined 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /testdata/account.json: -------------------------------------------------------------------------------- 1 | { 2 | "Account": { 3 | "Account Name": "Firefly", 4 | "Order": [ 5 | { 6 | "OrderID": "order103", 7 | "Product": [ 8 | { 9 | "Product Name": "Bowler Hat", 10 | "ProductID": 858383, 11 | "SKU": "0406654608", 12 | "Description": { 13 | "Colour": "Purple", 14 | "Width": 300, 15 | "Height": 200, 16 | "Depth": 210, 17 | "Weight": 0.75 18 | }, 19 | "Price": 34.45, 20 | "Quantity": 2 21 | }, 22 | { 23 | "Product Name": "Trilby hat", 24 | "ProductID": 858236, 25 | "SKU": "0406634348", 26 | "Description": { 27 | "Colour": "Orange", 28 | "Width": 300, 29 | "Height": 200, 30 | "Depth": 210, 31 | "Weight": 0.6 32 | }, 33 | "Price": 21.67, 34 | "Quantity": 1 35 | } 36 | ] 37 | }, 38 | { 39 | "OrderID": "order104", 40 | "Product": [ 41 | { 42 | "Product Name": "Bowler Hat", 43 | "ProductID": 858383, 44 | "SKU": "040657863", 45 | "Description": { 46 | "Colour": "Purple", 47 | "Width": 300, 48 | "Height": 200, 49 | "Depth": 210, 50 | "Weight": 0.75 51 | }, 52 | "Price": 34.45, 53 | "Quantity": 4 54 | }, 55 | { 56 | "ProductID": 345664, 57 | "SKU": "0406654603", 58 | "Product Name": "Cloak", 59 | "Description": { 60 | "Colour": "Black", 61 | "Width": 30, 62 | "Height": 20, 63 | "Depth": 210, 64 | "Weight": 2.0 65 | }, 66 | "Price": 107.99, 67 | "Quantity": 1 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /testdata/account2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Copy of account.json where Account.Order[0].Product[0] has no Price field.", 3 | "Account": { 4 | "Account Name": "Firefly", 5 | "Order": [ 6 | { 7 | "OrderID": "order103", 8 | "Product": [ 9 | { 10 | "Product Name": "Bowler Hat", 11 | "ProductID": 858383, 12 | "SKU": "0406654608", 13 | "Description": { 14 | "Colour": "Purple", 15 | "Width": 300, 16 | "Height": 200, 17 | "Depth": 210, 18 | "Weight": 0.75 19 | }, 20 | "Quantity": 2 21 | }, 22 | { 23 | "Product Name": "Trilby hat", 24 | "ProductID": 858236, 25 | "SKU": "0406634348", 26 | "Description": { 27 | "Colour": "Orange", 28 | "Width": 300, 29 | "Height": 200, 30 | "Depth": 210, 31 | "Weight": 0.6 32 | }, 33 | "Price": 21.67, 34 | "Quantity": 1 35 | } 36 | ] 37 | }, 38 | { 39 | "OrderID": "order104", 40 | "Product": [ 41 | { 42 | "Product Name": "Bowler Hat", 43 | "ProductID": 858383, 44 | "SKU": "040657863", 45 | "Description": { 46 | "Colour": "Purple", 47 | "Width": 300, 48 | "Height": 200, 49 | "Depth": 210, 50 | "Weight": 0.75 51 | }, 52 | "Price": 34.45, 53 | "Quantity": 4 54 | }, 55 | { 56 | "ProductID": 345664, 57 | "SKU": "0406654603", 58 | "Product Name": "Cloak", 59 | "Description": { 60 | "Colour": "Black", 61 | "Width": 30, 62 | "Height": 20, 63 | "Depth": 210, 64 | "Weight": 2.0 65 | }, 66 | "Price": 107.99, 67 | "Quantity": 1 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /testdata/account3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Copy of account.json where Account.Order[0].Product[1] has no Price field.", 3 | "Account": { 4 | "Account Name": "Firefly", 5 | "Order": [ 6 | { 7 | "OrderID": "order103", 8 | "Product": [ 9 | { 10 | "Product Name": "Bowler Hat", 11 | "ProductID": 858383, 12 | "SKU": "0406654608", 13 | "Description": { 14 | "Colour": "Purple", 15 | "Width": 300, 16 | "Height": 200, 17 | "Depth": 210, 18 | "Weight": 0.75 19 | }, 20 | "Price": 34.45, 21 | "Quantity": 2 22 | }, 23 | { 24 | "Product Name": "Trilby hat", 25 | "ProductID": 858236, 26 | "SKU": "0406634348", 27 | "Description": { 28 | "Colour": "Orange", 29 | "Width": 300, 30 | "Height": 200, 31 | "Depth": 210, 32 | "Weight": 0.6 33 | }, 34 | "Quantity": 1 35 | } 36 | ] 37 | }, 38 | { 39 | "OrderID": "order104", 40 | "Product": [ 41 | { 42 | "Product Name": "Bowler Hat", 43 | "ProductID": 858383, 44 | "SKU": "040657863", 45 | "Description": { 46 | "Colour": "Purple", 47 | "Width": 300, 48 | "Height": 200, 49 | "Depth": 210, 50 | "Weight": 0.75 51 | }, 52 | "Price": 34.45, 53 | "Quantity": 4 54 | }, 55 | { 56 | "ProductID": 345664, 57 | "SKU": "0406654603", 58 | "Product Name": "Cloak", 59 | "Description": { 60 | "Colour": "Black", 61 | "Width": 30, 62 | "Height": 20, 63 | "Depth": 210, 64 | "Weight": 2.0 65 | }, 66 | "Price": 107.99, 67 | "Quantity": 1 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /testdata/account4.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Copy of account.json where Account.Order[0].Product[0] and Account.Order[0].Product[1] have no Price field.", 3 | "Account": { 4 | "Account Name": "Firefly", 5 | "Order": [ 6 | { 7 | "OrderID": "order103", 8 | "Product": [ 9 | { 10 | "Product Name": "Bowler Hat", 11 | "ProductID": 858383, 12 | "SKU": "0406654608", 13 | "Description": { 14 | "Colour": "Purple", 15 | "Width": 300, 16 | "Height": 200, 17 | "Depth": 210, 18 | "Weight": 0.75 19 | }, 20 | "Quantity": 2 21 | }, 22 | { 23 | "Product Name": "Trilby hat", 24 | "ProductID": 858236, 25 | "SKU": "0406634348", 26 | "Description": { 27 | "Colour": "Orange", 28 | "Width": 300, 29 | "Height": 200, 30 | "Depth": 210, 31 | "Weight": 0.6 32 | }, 33 | "Quantity": 1 34 | } 35 | ] 36 | }, 37 | { 38 | "OrderID": "order104", 39 | "Product": [ 40 | { 41 | "Product Name": "Bowler Hat", 42 | "ProductID": 858383, 43 | "SKU": "040657863", 44 | "Description": { 45 | "Colour": "Purple", 46 | "Width": 300, 47 | "Height": 200, 48 | "Depth": 210, 49 | "Weight": 0.75 50 | }, 51 | "Price": 34.45, 52 | "Quantity": 4 53 | }, 54 | { 55 | "ProductID": 345664, 56 | "SKU": "0406654603", 57 | "Product Name": "Cloak", 58 | "Description": { 59 | "Colour": "Black", 60 | "Width": 30, 61 | "Height": 20, 62 | "Depth": 210, 63 | "Weight": 2.0 64 | }, 65 | "Price": 107.99, 66 | "Quantity": 1 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /testdata/account5.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Copy of account.json where Account.Order[0].Product[0].Price is a string instead of a number.", 3 | "Account": { 4 | "Account Name": "Firefly", 5 | "Order": [ 6 | { 7 | "OrderID": "order103", 8 | "Product": [ 9 | { 10 | "Product Name": "Bowler Hat", 11 | "ProductID": 858383, 12 | "SKU": "0406654608", 13 | "Description": { 14 | "Colour": "Purple", 15 | "Width": 300, 16 | "Height": 200, 17 | "Depth": 210, 18 | "Weight": 0.75 19 | }, 20 | "Price": "foo", 21 | "Quantity": 2 22 | }, 23 | { 24 | "Product Name": "Trilby hat", 25 | "ProductID": 858236, 26 | "SKU": "0406634348", 27 | "Description": { 28 | "Colour": "Orange", 29 | "Width": 300, 30 | "Height": 200, 31 | "Depth": 210, 32 | "Weight": 0.6 33 | }, 34 | "Price": 21.67, 35 | "Quantity": 1 36 | } 37 | ] 38 | }, 39 | { 40 | "OrderID": "order104", 41 | "Product": [ 42 | { 43 | "Product Name": "Bowler Hat", 44 | "ProductID": 858383, 45 | "SKU": "040657863", 46 | "Description": { 47 | "Colour": "Purple", 48 | "Width": 300, 49 | "Height": 200, 50 | "Depth": 210, 51 | "Weight": 0.75 52 | }, 53 | "Price": 34.45, 54 | "Quantity": 4 55 | }, 56 | { 57 | "ProductID": 345664, 58 | "SKU": "0406654603", 59 | "Product Name": "Cloak", 60 | "Description": { 61 | "Colour": "Black", 62 | "Width": 30, 63 | "Height": 20, 64 | "Depth": 210, 65 | "Weight": 2.0 66 | }, 67 | "Price": 107.99, 68 | "Quantity": 1 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /testdata/account6.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Copy of account.json where Account.Order[0].Product[0].Price is a boolean instead of a number.", 3 | "Account": { 4 | "Account Name": "Firefly", 5 | "Order": [ 6 | { 7 | "OrderID": "order103", 8 | "Product": [ 9 | { 10 | "Product Name": "Bowler Hat", 11 | "ProductID": 858383, 12 | "SKU": "0406654608", 13 | "Description": { 14 | "Colour": "Purple", 15 | "Width": 300, 16 | "Height": 200, 17 | "Depth": 210, 18 | "Weight": 0.75 19 | }, 20 | "Price": true, 21 | "Quantity": 2 22 | }, 23 | { 24 | "Product Name": "Trilby hat", 25 | "ProductID": 858236, 26 | "SKU": "0406634348", 27 | "Description": { 28 | "Colour": "Orange", 29 | "Width": 300, 30 | "Height": 200, 31 | "Depth": 210, 32 | "Weight": 0.6 33 | }, 34 | "Price": 21.67, 35 | "Quantity": 1 36 | } 37 | ] 38 | }, 39 | { 40 | "OrderID": "order104", 41 | "Product": [ 42 | { 43 | "Product Name": "Bowler Hat", 44 | "ProductID": 858383, 45 | "SKU": "040657863", 46 | "Description": { 47 | "Colour": "Purple", 48 | "Width": 300, 49 | "Height": 200, 50 | "Depth": 210, 51 | "Weight": 0.75 52 | }, 53 | "Price": 34.45, 54 | "Quantity": 4 55 | }, 56 | { 57 | "ProductID": 345664, 58 | "SKU": "0406654603", 59 | "Product Name": "Cloak", 60 | "Description": { 61 | "Colour": "Black", 62 | "Width": 30, 63 | "Height": 20, 64 | "Depth": 210, 65 | "Weight": 2.0 66 | }, 67 | "Price": 107.99, 68 | "Quantity": 1 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /testdata/account7.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Copy of account.json where Account.Order[0].Product[1].Price is null instead of a number.", 3 | "Account": { 4 | "Account Name": "Firefly", 5 | "Order": [ 6 | { 7 | "OrderID": "order103", 8 | "Product": [ 9 | { 10 | "Product Name": "Bowler Hat", 11 | "ProductID": 858383, 12 | "SKU": "0406654608", 13 | "Description": { 14 | "Colour": "Purple", 15 | "Width": 300, 16 | "Height": 200, 17 | "Depth": 210, 18 | "Weight": 0.75 19 | }, 20 | "Price": 34.45, 21 | "Quantity": 2 22 | }, 23 | { 24 | "Product Name": "Trilby hat", 25 | "ProductID": 858236, 26 | "SKU": "0406634348", 27 | "Description": { 28 | "Colour": "Orange", 29 | "Width": 300, 30 | "Height": 200, 31 | "Depth": 210, 32 | "Weight": 0.6 33 | }, 34 | "Price": null, 35 | "Quantity": 1 36 | } 37 | ] 38 | }, 39 | { 40 | "OrderID": "order104", 41 | "Product": [ 42 | { 43 | "Product Name": "Bowler Hat", 44 | "ProductID": 858383, 45 | "SKU": "040657863", 46 | "Description": { 47 | "Colour": "Purple", 48 | "Width": 300, 49 | "Height": 200, 50 | "Depth": 210, 51 | "Weight": 0.75 52 | }, 53 | "Price": 34.45, 54 | "Quantity": 4 55 | }, 56 | { 57 | "ProductID": 345664, 58 | "SKU": "0406654603", 59 | "Product Name": "Cloak", 60 | "Description": { 61 | "Colour": "Black", 62 | "Width": 30, 63 | "Height": 20, 64 | "Depth": 210, 65 | "Weight": 2.0 66 | }, 67 | "Price": 107.99, 68 | "Quantity": 1 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /testdata/address.json: -------------------------------------------------------------------------------- 1 | { 2 | "FirstName": "Fred", 3 | "Surname": "Smith", 4 | "Age": 28, 5 | "Address": { 6 | "Street": "Hursley Park", 7 | "City": "Winchester", 8 | "Postcode": "SO21 2JN" 9 | }, 10 | "Phone": [ 11 | { 12 | "type": "home", 13 | "number": "0203 544 1234" 14 | }, 15 | { 16 | "type": "office", 17 | "number": "01962 001234" 18 | }, 19 | { 20 | "type": "office", 21 | "number": "01962 001235" 22 | }, 23 | { 24 | "type": "mobile", 25 | "number": "077 7700 1234" 26 | } 27 | ], 28 | "Email": [ 29 | { 30 | "type": "work", 31 | "address": ["fred.smith@my-work.com", "fsmith@my-work.com"] 32 | }, 33 | { 34 | "type": "home", 35 | "address": ["freddy@my-social.com", "frederic.smith@very-serious.com"] 36 | } 37 | ], 38 | "Other": { 39 | "Over 18 ?": true, 40 | "Misc": null, 41 | "Alternative.Address": { 42 | "Street": "Brick Lane", 43 | "City": "London", 44 | "Postcode": "E1 6RF" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /testdata/foobar.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": { 3 | "bar": 42, 4 | "blah": [{"baz": {"fud": "hello"}}, {"baz": {"fud": "world"}}, {"bazz": "gotcha"}], 5 | "blah.baz": "here" 6 | }, 7 | "bar": 98 8 | } 9 | -------------------------------------------------------------------------------- /testdata/foobar2.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": { 3 | "bar": 42, 4 | "blah": [{"baz": {"fud": "hello"}}, {"buz": {"fud": "world"}}, {"bazz": "gotcha"}], 5 | "blah.baz": "here" 6 | }, "bar": 98 7 | } 8 | -------------------------------------------------------------------------------- /testdata/library.json: -------------------------------------------------------------------------------- 1 | { 2 | "library": { 3 | "books": [ 4 | { 5 | "title": "Structure and Interpretation of Computer Programs", 6 | "authors": ["Abelson", "Sussman"], 7 | "isbn": "9780262510875", 8 | "price": 38.90, 9 | "copies": 2 10 | }, 11 | { 12 | "title": "The C Programming Language", 13 | "authors": ["Kernighan", "Richie"], 14 | "isbn": "9780131103627", 15 | "price": 33.59, 16 | "copies": 3 17 | }, 18 | { 19 | "title": "The AWK Programming Language", 20 | "authors": ["Aho", "Kernighan", "Weinberger"], 21 | "isbn": "9780201079814", 22 | "copies": 1 23 | }, 24 | { 25 | "title": "Compilers: Principles, Techniques, and Tools", 26 | "authors": ["Aho", "Lam", "Sethi", "Ullman"], 27 | "isbn": "9780201100884", 28 | "price": 23.38, 29 | "copies": 1 30 | } 31 | ], 32 | "loans": [ 33 | { 34 | "customer": "10001", 35 | "isbn": "9780262510875", 36 | "return": "2016-12-05" 37 | }, 38 | { 39 | "customer": "10003", 40 | "isbn": "9780201100884", 41 | "return": "2016-10-22" 42 | } 43 | ], 44 | "customers": [ 45 | { 46 | "id": "10001", 47 | "name": "Joe Doe", 48 | "address": { 49 | "street": "2 Long Road", 50 | "city": "Winchester", 51 | "postcode": "SO22 5PU" 52 | } 53 | }, 54 | { 55 | "id": "10002", 56 | "name": "Fred Bloggs", 57 | "address": { 58 | "street": "56 Letsby Avenue", 59 | "city": "Winchester", 60 | "postcode": "SO22 4WD" 61 | } 62 | }, 63 | { 64 | "id": "10003", 65 | "name": "Jason Arthur", 66 | "address": { 67 | "street": "1 Preddy Gate", 68 | "city": "Southampton", 69 | "postcode": "SO14 0MG" 70 | } 71 | } 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /testdata/nest1.json: -------------------------------------------------------------------------------- 1 | { 2 | "nest0": [ 3 | {"nest1": [{"nest2": [{"nest3": [1]}, {"nest3": [2]}]}, {"nest2": [{"nest3": [3]}, {"nest3": [4]}]}]}, 4 | {"nest1": [{"nest2": [{"nest3": [5]}, {"nest3": [6]}]}, {"nest2": [{"nest3": [7]}, {"nest3": [8]}]}]} 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /testdata/nest2.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"nest0": [1, 2]}, 3 | {"nest0": [3, 4]} 4 | ] 5 | -------------------------------------------------------------------------------- /testdata/nest3.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"nest0": [{"nest1": [1, 2]}, {"nest1": [3, 4]}]}, 3 | {"nest0": [{"nest1": [5]}, {"nest1": [6]}]} 4 | ] 5 | --------------------------------------------------------------------------------