├── .github
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── .goreleaser.yml
├── .travis.yml
├── ADVANCED.mkd
├── CHANGELOG.mkd
├── CONTRIBUTING.mkd
├── LICENSE
├── README.mkd
├── completions
├── gron.bash
└── gron.fish
├── docs
├── images
│ ├── bareword.png
│ ├── input.png
│ ├── key.png
│ ├── path.png
│ ├── statement.png
│ ├── string.png
│ ├── unescapedrune.png
│ └── value.png
└── index.html
├── go.mod
├── go.sum
├── identifier.go
├── identifier_test.go
├── main.go
├── main_test.go
├── original-gron.php
├── script
├── example
├── lint
├── precommit
├── profile
├── release
└── test
├── statements.go
├── statements_test.go
├── testdata
├── big.json
├── github.gron
├── github.jgron
├── github.json
├── grep-separators.gron
├── grep-separators.json
├── invalid-type-mismatch.gron
├── invalid-value.gron
├── large-line.gron
├── large-line.json
├── long-stream.gron
├── long-stream.json
├── one.gron
├── one.jgron
├── one.json
├── scalar-stream.gron
├── scalar-stream.jgron
├── scalar-stream.json
├── stream.gron
├── stream.jgron
├── stream.json
├── three.gron
├── three.jgron
├── three.json
├── two-b.json
├── two.gron
├── two.jgron
└── two.json
├── token.go
├── token_test.go
├── ungron.go
├── ungron_test.go
├── url.go
└── url_test.go
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | workflow_dispatch:
8 | jobs:
9 |
10 | build:
11 | strategy:
12 | matrix:
13 | go-version: ['stable', 'oldstable']
14 | os: [ubuntu-latest]
15 | runs-on: ${{ matrix.os }}
16 |
17 | steps:
18 | - name: Check out code into the Go module directory
19 | uses: actions/checkout@v4
20 | - name: Install Go
21 | if: success()
22 | uses: actions/setup-go@v5
23 | with:
24 | go-version: ${{ matrix.go-version }}
25 | cache: true
26 |
27 | - name: Run tests
28 | run: go test
29 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 | jobs:
7 | goreleaser:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 | with:
13 | fetch-depth: 0
14 | - name: Setup Go
15 | uses: actions/setup-go@v5
16 | with:
17 | go-version: 'stable'
18 | - name: Run GoReleaser
19 | uses: goreleaser/goreleaser-action@v6
20 | with:
21 | version: latest
22 | args: release --clean
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 | TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | gron
2 | *.tgz
3 | *.zip
4 | *.swp
5 | *.exe
6 | cpu.out
7 | gron.test
8 | vendor
9 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | before:
4 | hooks:
5 | - go mod tidy
6 | - go mod vendor
7 |
8 | source:
9 | enabled: true
10 | files:
11 | - vendor
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - "1.13"
5 | - "1.14"
6 | - "1.15"
7 | - "1.16"
8 | - "1.17"
9 | - "1.18"
10 | - "1.19"
11 | - "1.20"
12 | - "1.21"
13 | - "1.22"
14 | - "1.23"
15 | - tip
16 |
--------------------------------------------------------------------------------
/ADVANCED.mkd:
--------------------------------------------------------------------------------
1 | # Advanced Usage
2 |
3 | Although gron's primary purpose is API discovery, when combined with other tools like `grep` it can do some interesting things.
4 |
5 | As an exercise, let's try to mimick some of the examples from the [jq tutorial](https://stedolan.github.io/jq/tutorial/).
6 |
7 | > Disclaimer: munging data on the command line with gron can be useful, but using tools like `grep` and `sed` to manipulate the
8 | > data is error-prone and shouldn't be relied on in scripts.
9 |
10 | Get the last 5 commits from the gron repo:
11 | ```
12 | ▶ gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=5"
13 | json = [];
14 | json[0] = {};
15 | json[0].author = {};
16 | json[0].author.avatar_url = "https://avatars.githubusercontent.com/u/58276?v=3";
17 | json[0].author.events_url = "https://api.github.com/users/tomnomnom/events{/privacy}";
18 | ...
19 | json[4].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/cbcad2299e55c28a9922776e58b2a0b5a0f05016";
20 | json[4].parents[0].sha = "cbcad2299e55c28a9922776e58b2a0b5a0f05016";
21 | json[4].parents[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/cbcad2299e55c28a9922776e58b2a0b5a0f05016";
22 | json[4].sha = "91b204972e63a1166c9d148fbbfd839f8697f91b";
23 | json[4].url = "https://api.github.com/repos/tomnomnom/gron/commits/91b204972e63a1166c9d148fbbfd839f8697f91b";
24 | ```
25 |
26 | To make the rest of this a little more readable, let's add an alias for that:
27 |
28 | ```
29 | ▶ alias ggh='gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=5"'
30 | ```
31 |
32 | Extract just the first commit using `fgrep "json[0]"`:
33 | ```
34 | ▶ ggh | fgrep "json[0]"
35 | json[0] = {};
36 | json[0].author = {};
37 | json[0].author.avatar_url = "https://avatars.githubusercontent.com/u/58276?v=3";
38 | json[0].author.events_url = "https://api.github.com/users/tomnomnom/events{/privacy}";
39 | json[0].author.followers_url = "https://api.github.com/users/tomnomnom/followers";
40 | ...
41 | json[0].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/48aba5325ece087ae24ab72684851cbe77ce8311";
42 | json[0].parents[0].sha = "48aba5325ece087ae24ab72684851cbe77ce8311";
43 | json[0].parents[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/48aba5325ece087ae24ab72684851cbe77ce8311";
44 | json[0].sha = "7da81e29c27241c0a5c2e5d083ddebcfcc525908";
45 | json[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/7da81e29c27241c0a5c2e5d083ddebcfcc525908";
46 | ```
47 |
48 | Get just the committer's name and the commit message using `egrep "(committer.name|commit.message)"`:
49 | ```
50 | ▶ ggh | fgrep "json[0]" | egrep "(committer.name|commit.message)"
51 | json[0].commit.committer.name = "Tom Hudson";
52 | json[0].commit.message = "Adds 0.1.7 to changelog";
53 | ```
54 |
55 | Turn the result back into JSON using `gron --ungron`:
56 | ```
57 | ▶ ggh | fgrep "json[0]" | egrep "(committer.name|commit.message)" | gron --ungron
58 | [
59 | {
60 | "commit": {
61 | "committer": {
62 | "name": "Tom Hudson"
63 | },
64 | "message": "Adds 0.1.7 to changelog"
65 | }
66 | }
67 | ]
68 | ```
69 |
70 | gron preserves the location of values in the JSON, but you can use `sed` to remove keys from the path:
71 | ```
72 | ▶ ggh | fgrep "json[0]" | egrep "(committer.name|commit.message)" | sed -r "s/(commit|committer)\.//g"
73 | json[0].name = "Tom Hudson";
74 | json[0].message = "Adds 0.1.7 to changelog"
75 |
76 | ```
77 |
78 | With those keys removed, the result is a 'flattened' object, which looks much cleaner when turned
79 | back into JSON with `gron --ungron`:
80 |
81 | ```
82 | ▶ ggh | fgrep "json[0]" | egrep "(committer.name|commit.message)" | sed -r "s/(commit|committer)\.//g" | gron --ungron
83 | [
84 | {
85 | "message": "Adds 0.1.7 to changelog",
86 | "name": "Tom Hudson"
87 | }
88 | ]
89 | ```
90 |
91 | Removing the `fgrep "json[0]"` from the pipeline means we do the same for all commits:
92 | ```
93 | ▶ ggh | egrep "(committer.name|commit.message)" | sed -r "s/(commit|committer)\.//g" | gron --ungron
94 | [
95 | {
96 | "message": "Adds 0.1.7 to changelog",
97 | "name": "Tom Hudson"
98 | },
99 | {
100 | "message": "Refactors natural sort to actually work + be more readable",
101 | "name": "Tom Hudson"
102 | },
103 | ...
104 | ```
105 |
106 | To include the `html_url` key for each commit's parents, all we need to do is add `parents.*html_url` into our call to `egrep`:
107 | ```
108 | ▶ ggh | egrep "(committer.name|commit.message|parents.*html_url)" | sed -r "s/(commit|committer)\.//g"
109 | json[0].name = "Tom Hudson";
110 | json[0].message = "Adds 0.1.7 to changelog";
111 | json[0].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/48aba5325ece087ae24ab72684851cbe77ce8311";
112 | json[1].name = "Tom Hudson";
113 | json[1].message = "Refactors natural sort to actually work + be more readable";
114 | json[1].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/3eca8bf5e07151f077cebf0d942c1fa8bc51e8f2";
115 | ...
116 | ```
117 |
118 | To make the structure more like that in the final example in the `jq` tutorial, we can use `sed -r "s/\.html_url//"` to remove the `.html_url` part of the path:
119 | ```
120 | ▶ ggh | egrep "(committer.name|commit.message|parents.*html_url)" | sed -r "s/(commit|committer)\.//g" | sed -r "s/\.html_url//"
121 | json[0].name = "Tom Hudson";
122 | json[0].message = "Adds 0.1.7 to changelog";
123 | json[0].parents[0] = "https://github.com/tomnomnom/gron/commit/48aba5325ece087ae24ab72684851cbe77ce8311";
124 | json[1].name = "Tom Hudson";
125 | json[1].message = "Refactors natural sort to actually work + be more readable";
126 | json[1].parents[0] = "https://github.com/tomnomnom/gron/commit/3eca8bf5e07151f077cebf0d942c1fa8bc51e8f2";
127 | ...
128 | ```
129 |
130 | And, of course, the statements can be turned back into JSON with `gron --ungron`:
131 | ```
132 | ▶ ggh | egrep "(committer.name|commit.message|parents.*html_url)" | sed -r "s/(commit|committer)\.//g" | sed -r "s/\.html_url//" | gron --ungron
133 | [
134 | {
135 | "message": "Adds 0.1.7 to changelog",
136 | "name": "Tom Hudson",
137 | "parents": [
138 | "https://github.com/tomnomnom/gron/commit/48aba5325ece087ae24ab72684851cbe77ce8311"
139 | ]
140 | },
141 | {
142 | "message": "Refactors natural sort to actually work + be more readable",
143 | "name": "Tom Hudson",
144 | "parents": [
145 | "https://github.com/tomnomnom/gron/commit/3eca8bf5e07151f077cebf0d942c1fa8bc51e8f2"
146 | ]
147 | },
148 | ...
149 | ```
150 |
--------------------------------------------------------------------------------
/CHANGELOG.mkd:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.6.0
4 | - Adds `--json`/JSON stream output support (thanks @csabahenk!)
5 | - Removes trailing newline character for monochrome output (issue #43)
6 |
7 | ## 0.5.2
8 | - Built with Go 1.10 to fix issue #32 - Thanks @VladimirAlexiev and @joekyo!
9 |
10 | ## 0.5.1
11 | - Fixes bug where empty identifiers would be treated as bare words (thanks for the report, @waltertross!)
12 |
13 | ## 0.5.0
14 | - Adds `-k`/`--insecure` to disable validation of certificates when fetching URLs (thanks @jagsta!)
15 |
16 | ## 0.4.0
17 | - Adds `-c`/`--colorize` to force colorization of output (thanks @orivej!)
18 | - Adds `-s`/`--stream` option to read one JSON object per line
19 | - Native string quoting (performance improvement)
20 | - Fixes bug with strings ending in a double-slash (issue #25)
21 |
22 | ## 0.3.7
23 | - HTML characters (`<`, `>` etc) are no-longer escaped in gron output (issue #22)
24 |
25 | ## 0.3.6
26 | - Fixes bug where invalid statements were outputted
27 |
28 | ## 0.3.5
29 | - General performance improvements; 5 to 17 times faster (see issue #21 for details)
30 |
31 | ## 0.3.4
32 | - Speed improvements when using `--monochrome`
33 | - Adds `--no-sort` option
34 |
35 | ## 0.3.3
36 | - Slightly improved error reporting when ungronning
37 | - 20 second timeout on HTTP(s) requests (thanks @gummiboll!)
38 | - Version information added at build time + `--version` option (issue #19)
39 |
40 | ## 0.3.2
41 | - Adds handling of `--` lines produced by grep -A etc (issue #15)
42 |
43 | ## 0.3.1
44 | - Built with Go 1.7!
45 | - Up to 25% faster
46 | - 40% smaller binaries
47 |
48 | ## 0.3.0
49 | - Adds colorized gron output
50 | - Fixes formatting of large ints in ungron output (issue #12)
51 |
52 | ## 0.2.9
53 | - Adds colorized ungron output (thanks @nwidger!)
54 | - Adds 32 bit binaries to releases
55 |
56 | ## 0.2.8
57 | - Adds freebsd release binaries
58 |
59 | ## 0.2.7
60 | - Fixes bad handling of escape sequences when ungronning - but properly this time (issue #7)
61 |
62 | ## 0.2.5
63 | - Fixes bad handling of escape sequences when ungronning (issue #7)
64 |
65 | ## 0.2.4
66 | - Fixes handling of large integers (issue #6)
67 |
68 | ## 0.2.3
69 | - Switches Windows binary packaging to zip instead of tgz
70 |
71 | ## 0.2.2
72 | - Tweaks release automation, no user-facing changes
73 |
74 | ## 0.2.1
75 | - Adds windows binary
76 |
77 | ## 0.2.0
78 | - Adds [ungronning](README.mkd#ungronning)!
79 |
80 | ## 0.1.7
81 | - Fixes sorting of array keys; now uses natural sort
82 |
83 | ## 0.1.6
84 | - Adds proper handling of key quoting using Unicode ranges
85 | - Adds basic benchmarks
86 | - Adds profiling script
87 |
88 | ## 0.1.5
89 | - Adds scripted builds for darwin on amd64
90 |
91 | ## 0.1.4
92 | - Minor changes to release script
93 |
94 | ## 0.1.3
95 | - Releases are now tarballs
96 |
97 | ## 0.1.2
98 | - Underscores no-longer cause keys to be quoted
99 | - HTTP requests are now done with `Accept: application/json`
100 | - HTTP requests are now done with `User-Agent: gron/0.1`
101 |
102 | ## 0.1.1
103 | - Adds support for fetching URLs directly
104 |
105 | ## 0.1.0
106 | - Support for files
107 | - Support for `stdin`
108 |
109 |
--------------------------------------------------------------------------------
/CONTRIBUTING.mkd:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | * Raise an issue if appropriate
4 | * Fork the repo
5 | * Make your changes
6 | * Use [gofmt](https://golang.org/cmd/gofmt/)
7 | * Make sure the tests pass (run `./script/test`)
8 | * Make sure the linters pass (run `./script/lint`)
9 | * Issue a pull request
10 |
11 | ## Commit Messages
12 | I'd prefer it if commit messages describe what the *commit does*, not what *you did*.
13 |
14 | For example, I'd prefer:
15 |
16 | ```
17 | Adds lint; removes fluff
18 | ```
19 |
20 | Over:
21 |
22 | ```
23 | Added lint; removed fluff
24 | ```
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Tom Hudson
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.mkd:
--------------------------------------------------------------------------------
1 | # gron
2 | [](https://travis-ci.org/tomnomnom/gron)
3 |
4 | Make JSON greppable!
5 |
6 | gron transforms JSON into discrete assignments to make it easier to `grep` for what you want and see the absolute 'path' to it.
7 | It eases the exploration of APIs that return large blobs of JSON but have terrible documentation.
8 |
9 |
10 | ▶ gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=1" | fgrep "commit.author"
11 | json[0].commit.author = {};
12 | json[0].commit.author.date = "2016-07-02T10:51:21Z";
13 | json[0].commit.author.email = "mail@tomnomnom.com";
14 | json[0].commit.author.name = "Tom Hudson";
15 |
16 |
17 | gron can work backwards too, enabling you to turn your filtered data back into JSON:
18 |
19 | ▶ gron "https://api.github.com/repos/tomnomnom/gron/commits?per_page=1" | fgrep "commit.author" | gron --ungron
20 | [
21 | {
22 | "commit": {
23 | "author": {
24 | "date": "2016-07-02T10:51:21Z",
25 | "email": "mail@tomnomnom.com",
26 | "name": "Tom Hudson"
27 | }
28 | }
29 | }
30 | ]
31 |
32 |
33 | > Disclaimer: the GitHub API has fantastic documentation, but it makes for a good example.
34 |
35 | ## Installation
36 |
37 | gron has no runtime dependencies. You can just [download a binary for Linux, Mac, Windows or FreeBSD and run it](https://github.com/tomnomnom/gron/releases).
38 | Put the binary in your `$PATH` (e.g. in `/usr/local/bin`) to make it easy to use:
39 | ```
40 | ▶ tar xzf gron-linux-amd64-0.1.5.tgz
41 | ▶ sudo mv gron /usr/local/bin/
42 | ```
43 |
44 | If you're a Mac user you can also [install gron via brew](http://braumeister.org/formula/gron):
45 | ```
46 | ▶ brew install gron
47 | ```
48 |
49 | Or if you're a Go user you can use `go install`:
50 |
51 | ```
52 | ▶ go install github.com/tomnomnom/gron@latest
53 | ```
54 |
55 | It's recommended that you alias `ungron` or `norg` (or both!) to `gron --ungron`. Put something like this in your shell profile (e.g. in `~/.bashrc`):
56 | ```
57 | alias norg="gron --ungron"
58 | alias ungron="gron --ungron"
59 | ```
60 | Or you could create a shell script in your $PATH named `ungron` or `norg` to affect all users:
61 | ```
62 | gron --ungron "$@"
63 | ```
64 |
65 | ## Usage
66 |
67 | Get JSON from a file:
68 |
69 | ```
70 | ▶ gron testdata/two.json
71 | json = {};
72 | json.contact = {};
73 | json.contact.email = "mail@tomnomnom.com";
74 | json.contact.twitter = "@TomNomNom";
75 | json.github = "https://github.com/tomnomnom/";
76 | json.likes = [];
77 | json.likes[0] = "code";
78 | json.likes[1] = "cheese";
79 | json.likes[2] = "meat";
80 | json.name = "Tom";
81 | ```
82 |
83 | From a URL:
84 |
85 | ```
86 | ▶ gron http://headers.jsontest.com/
87 | json = {};
88 | json.Host = "headers.jsontest.com";
89 | json["User-Agent"] = "gron/0.1";
90 | json["X-Cloud-Trace-Context"] = "6917a823919477919dbc1523584ba25d/11970839830843610056";
91 | ```
92 |
93 | Or from `stdin`:
94 |
95 | ```
96 | ▶ curl -s http://headers.jsontest.com/ | gron
97 | json = {};
98 | json.Accept = "*/*";
99 | json.Host = "headers.jsontest.com";
100 | json["User-Agent"] = "curl/7.43.0";
101 | json["X-Cloud-Trace-Context"] = "c70f7bf26661c67d0b9f2cde6f295319/13941186890243645147";
102 | ```
103 |
104 | Grep for something and easily see the path to it:
105 |
106 | ```
107 | ▶ gron testdata/two.json | grep twitter
108 | json.contact.twitter = "@TomNomNom";
109 | ```
110 |
111 | gron makes diffing JSON easy too:
112 |
113 | ```
114 | ▶ diff <(gron two.json) <(gron two-b.json)
115 | 3c3
116 | < json.contact.email = "mail@tomnomnom.com";
117 | ---
118 | > json.contact.email = "contact@tomnomnom.com";
119 | ```
120 |
121 | The output of `gron` is valid JavaScript:
122 |
123 | ```
124 | ▶ gron testdata/two.json > tmp.js
125 | ▶ echo "console.log(json);" >> tmp.js
126 | ▶ nodejs tmp.js
127 | { contact: { email: 'mail@tomnomnom.com', twitter: '@TomNomNom' },
128 | github: 'https://github.com/tomnomnom/',
129 | likes: [ 'code', 'cheese', 'meat' ],
130 | name: 'Tom' }
131 | ```
132 |
133 | It's also possible to obtain the `gron` output as JSON stream via
134 | the `--json` switch:
135 |
136 | ```
137 | ▶ curl -s http://headers.jsontest.com/ | gron --json
138 | [[],{}]
139 | [["Accept"],"*/*"]
140 | [["Host"],"headers.jsontest.com"]
141 | [["User-Agent"],"curl/7.43.0"]
142 | [["X-Cloud-Trace-Context"],"c70f7bf26661c67d0b9f2cde6f295319/13941186890243645147"]
143 | ```
144 |
145 | ## ungronning
146 | gron can also turn its output back into JSON:
147 | ```
148 | ▶ gron testdata/two.json | gron -u
149 | {
150 | "contact": {
151 | "email": "mail@tomnomnom.com",
152 | "twitter": "@TomNomNom"
153 | },
154 | "github": "https://github.com/tomnomnom/",
155 | "likes": [
156 | "code",
157 | "cheese",
158 | "meat"
159 | ],
160 | "name": "Tom"
161 | }
162 | ```
163 |
164 | This means you use can use gron with `grep` and other tools to modify JSON:
165 | ```
166 | ▶ gron testdata/two.json | grep likes | gron --ungron
167 | {
168 | "likes": [
169 | "code",
170 | "cheese",
171 | "meat"
172 | ]
173 | }
174 | ```
175 |
176 | or
177 |
178 |
179 | ```
180 | ▶ gron --json testdata/two.json | grep likes | gron --json --ungron
181 | {
182 | "likes": [
183 | "code",
184 | "cheese",
185 | "meat"
186 | ]
187 | }
188 | ```
189 |
190 | To preserve array keys, arrays are padded with `null` when values are missing:
191 | ```
192 | ▶ gron testdata/two.json | grep likes | grep -v cheese
193 | json.likes = [];
194 | json.likes[0] = "code";
195 | json.likes[2] = "meat";
196 | ▶ gron testdata/two.json | grep likes | grep -v cheese | gron --ungron
197 | {
198 | "likes": [
199 | "code",
200 | null,
201 | "meat"
202 | ]
203 | }
204 | ```
205 |
206 | If you get creative you can do [some pretty neat tricks with gron](ADVANCED.mkd), and
207 | then ungron the output back into JSON.
208 |
209 | ## Get Help
210 |
211 | ```
212 | ▶ gron --help
213 | Transform JSON (from a file, URL, or stdin) into discrete assignments to make it greppable
214 |
215 | Usage:
216 | gron [OPTIONS] [FILE|URL|-]
217 |
218 | Options:
219 | -u, --ungron Reverse the operation (turn assignments back into JSON)
220 | -v, --values Print just the values of provided assignments
221 | -c, --colorize Colorize output (default on tty)
222 | -m, --monochrome Monochrome (don't colorize output)
223 | -s, --stream Treat each line of input as a separate JSON object
224 | -k, --insecure Disable certificate validation
225 | -j, --json Represent gron data as JSON stream
226 | --no-sort Don't sort output (faster)
227 | --version Print version information
228 |
229 | Exit Codes:
230 | 0 OK
231 | 1 Failed to open file
232 | 2 Failed to read input
233 | 3 Failed to form statements
234 | 4 Failed to fetch URL
235 | 5 Failed to parse statements
236 | 6 Failed to encode JSON
237 |
238 | Examples:
239 | gron /tmp/apiresponse.json
240 | gron http://jsonplaceholder.typicode.com/users/1
241 | curl -s http://jsonplaceholder.typicode.com/users/1 | gron
242 | gron http://jsonplaceholder.typicode.com/users/1 | grep company | gron --ungron
243 | ```
244 |
245 | ## FAQ
246 | ### Wasn't this written in PHP before?
247 | Yes it was! The original version is [preserved here for posterity](https://github.com/tomnomnom/gron/blob/master/original-gron.php).
248 |
249 | ### Why the change to Go?
250 | Mostly to remove PHP as a dependency. There's a lot of people who work with JSON who don't have PHP installed.
251 |
252 | ### Why shouldn't I just use jq?
253 | [jq](https://stedolan.github.io/jq/) is *awesome*, and a lot more powerful than gron, but with that power comes
254 | complexity. gron aims to make it easier to use the tools you already know, like `grep` and `sed`.
255 |
256 | gron's primary purpose is to make it easy to find the path to a value in a deeply nested JSON blob
257 | when you don't already know the structure; much of jq's power is unlocked only once you know that structure.
258 |
--------------------------------------------------------------------------------
/completions/gron.bash:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Bash shell commandline completions for gron.
4 | #
5 | # Copy the contents of this file into your ~/.bashrc file or whatever
6 | # file you use for Bash completions.
7 | #
8 | # Example: cat ./completions/gron.bash >> ~/.bashrc
9 |
10 | function _gron_completion {
11 | local AVAILABLE_COMMANDS="--colorize --insecure --json --monochrome --no-sort --stream --ungron --values --version"
12 | COMPREPLY=()
13 |
14 | local CURRENT_WORD=${COMP_WORDS[COMP_CWORD]}
15 | COMPREPLY=($(compgen -W "$AVAILABLE_COMMANDS" -- "$CURRENT_WORD"))
16 | }
17 |
18 | complete -F _gron_completion gron
19 |
--------------------------------------------------------------------------------
/completions/gron.fish:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env fish
2 |
3 | # Fish shell commandline completions for gron.
4 | #
5 | # Stick this file in your ~/.config/fish/completions/ directory.
6 |
7 | complete -c gron -s u -l ungron --description "Reverse the operation (turn assignments back into JSON)"
8 | complete -c gron -s c -l colorize --description "Colorize output (default on tty)"
9 | complete -c gron -s m -l monochrome --description "Monochrome (don't colorize output)"
10 | complete -c gron -s s -l stream --description "Treat each line of input as a separate JSON object"
11 | complete -c gron -s k -l insecure --description "Disable certificate validation"
12 | complete -c gron -s j -l json --description "Represent gron data as JSON stream"
13 | complete -c gron -l no-sort --description "Don't sort output (faster)"
14 | complete -c gron -l version --description "Print version information"
15 |
16 | # eof
17 |
--------------------------------------------------------------------------------
/docs/images/bareword.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/bareword.png
--------------------------------------------------------------------------------
/docs/images/input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/input.png
--------------------------------------------------------------------------------
/docs/images/key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/key.png
--------------------------------------------------------------------------------
/docs/images/path.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/path.png
--------------------------------------------------------------------------------
/docs/images/statement.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/statement.png
--------------------------------------------------------------------------------
/docs/images/string.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/string.png
--------------------------------------------------------------------------------
/docs/images/unescapedrune.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/unescapedrune.png
--------------------------------------------------------------------------------
/docs/images/value.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tomnomnom/gron/88a6234ea2d0c487090988182ad9a7cdf6def924/docs/images/value.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | gron
5 |
6 |
7 |
8 | gron
9 | Ungron Input Grammar
10 |
11 |
12 | Input ::= '--'* Statement (Statement | '--')*
13 | Statement ::= Path Space* "=" Space* Value ";" "\n"
14 | Path ::= (BareWord) ("." BareWord | ("[" Key "]"))*
15 | Value ::= String | Number | "true" | "false" | "null" | "[]" | "{}"
16 | BareWord ::= (UnicodeLu | UnicodeLl | UnicodeLm | UnicodeLo | UnicodeNl | '$' | '_') (UnicodeLu | UnicodeLl | UnicodeLm | UnicodeLo | UnicodeNl | UnicodeMn | UnicodeMc | UnicodeNd | UnicodePc | '$' | '_')*
17 | Key ::= [0-9]+ | String
18 | String ::= '"' (UnescapedRune | ("\" (["\/bfnrt] | ('u' Hex))))* '"'
19 | UnescapedRune ::= [^#x0-#x1f"\]
20 |
21 |
22 | Input
23 |
24 |
25 | Statement
26 |
27 |
28 | Path
29 |
30 |
31 | Value
32 |
33 |
34 | BareWord
35 |
36 |
37 | Key
38 |
39 |
40 | String
41 |
42 |
43 | UnescapedRune
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tomnomnom/gron
2 |
3 | go 1.24
4 |
5 | require (
6 | github.com/fatih/color v1.18.0
7 | github.com/mattn/go-colorable v0.1.14
8 | github.com/nwidger/jsoncolor v0.3.2
9 | github.com/pkg/errors v0.9.1
10 | )
11 |
12 | require (
13 | github.com/mattn/go-isatty v0.0.20 // indirect
14 | golang.org/x/sys v0.33.0 // indirect
15 | )
16 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
2 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
3 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
4 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
5 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
6 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
7 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
8 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
9 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
10 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
11 | github.com/nwidger/jsoncolor v0.3.2 h1:rVJJlwAWDJShnbTYOQ5RM7yTA20INyKXlJ/fg4JMhHQ=
12 | github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4=
13 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
14 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
15 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
16 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
17 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
18 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
19 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
20 |
--------------------------------------------------------------------------------
/identifier.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "unicode"
4 |
5 | // The javascript reserved words cannot be used as unquoted keys
6 | var reservedWords = map[string]bool{
7 | "break": true,
8 | "case": true,
9 | "catch": true,
10 | "class": true,
11 | "const": true,
12 | "continue": true,
13 | "debugger": true,
14 | "default": true,
15 | "delete": true,
16 | "do": true,
17 | "else": true,
18 | "export": true,
19 | "extends": true,
20 | "false": true,
21 | "finally": true,
22 | "for": true,
23 | "function": true,
24 | "if": true,
25 | "import": true,
26 | "in": true,
27 | "instanceof": true,
28 | "new": true,
29 | "null": true,
30 | "return": true,
31 | "super": true,
32 | "switch": true,
33 | "this": true,
34 | "throw": true,
35 | "true": true,
36 | "try": true,
37 | "typeof": true,
38 | "var": true,
39 | "void": true,
40 | "while": true,
41 | "with": true,
42 | "yield": true,
43 | }
44 |
45 | // validIdentifier checks to see if a string is a valid
46 | // JavaScript identifier
47 | // E.g:
48 | //
49 | // justLettersAndNumbers1 -> true
50 | // a key with spaces -> false
51 | // 1startsWithANumber -> false
52 | func validIdentifier(s string) bool {
53 | if reservedWords[s] || s == "" {
54 | return false
55 | }
56 |
57 | for i, r := range s {
58 | if i == 0 && !validFirstRune(r) {
59 | return false
60 | }
61 | if i != 0 && !validSecondaryRune(r) {
62 | return false
63 | }
64 | }
65 |
66 | return true
67 | }
68 |
69 | // validFirstRune returns true for runes that are valid
70 | // as the first rune in an identifier.
71 | // E.g:
72 | //
73 | // 'r' -> true
74 | // '7' -> false
75 | func validFirstRune(r rune) bool {
76 | return unicode.In(r,
77 | unicode.Lu,
78 | unicode.Ll,
79 | unicode.Lm,
80 | unicode.Lo,
81 | unicode.Nl,
82 | ) || r == '$' || r == '_'
83 | }
84 |
85 | // validSecondaryRune returns true for runes that are valid
86 | // as anything other than the first rune in an identifier.
87 | func validSecondaryRune(r rune) bool {
88 | return validFirstRune(r) ||
89 | unicode.In(r, unicode.Mn, unicode.Mc, unicode.Nd, unicode.Pc)
90 | }
91 |
--------------------------------------------------------------------------------
/identifier_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "testing"
4 |
5 | func TestValidIdentifier(t *testing.T) {
6 | tests := []struct {
7 | key string
8 | want bool
9 | }{
10 | // Valid Identifiers
11 | {"dotted", true},
12 | {"dotted123", true},
13 | {"_under_scores", true},
14 | {"ಠ_ಠ", true},
15 |
16 | // Invalid chars
17 | {"is-quoted", false},
18 | {"Definitely quoted!", false},
19 |
20 | // Reserved words
21 | {"true", false},
22 | {"else", false},
23 | {"null", false},
24 |
25 | // Empty string
26 | {"", false},
27 | }
28 |
29 | for _, test := range tests {
30 | have := validIdentifier(test.key)
31 | if have != test.want {
32 | t.Errorf("Want %t for validIdentifier(%s); have %t", test.want, test.key, have)
33 | }
34 | }
35 | }
36 |
37 | func TestValidFirstRune(t *testing.T) {
38 | tests := []struct {
39 | in rune
40 | want bool
41 | }{
42 | {'r', true},
43 | {'ಠ', true},
44 | {'4', false},
45 | {'-', false},
46 | }
47 |
48 | for _, test := range tests {
49 | have := validFirstRune(test.in)
50 | if have != test.want {
51 | t.Errorf("Want %t for validFirstRune(%#U); have %t", test.want, test.in, have)
52 | }
53 | }
54 | }
55 |
56 | func TestValidSecondaryRune(t *testing.T) {
57 | tests := []struct {
58 | in rune
59 | want bool
60 | }{
61 | {'r', true},
62 | {'ಠ', true},
63 | {'4', true},
64 | {'-', false},
65 | }
66 |
67 | for _, test := range tests {
68 | have := validSecondaryRune(test.in)
69 | if have != test.want {
70 | t.Errorf("Want %t for validSecondaryRune(%#U); have %t", test.want, test.in, have)
71 | }
72 | }
73 | }
74 |
75 | func BenchmarkValidIdentifier(b *testing.B) {
76 | for i := 0; i < b.N; i++ {
77 | validIdentifier("must-be-quoted")
78 | }
79 | }
80 |
81 | func BenchmarkValidIdentifierUnquoted(b *testing.B) {
82 | for i := 0; i < b.N; i++ {
83 | validIdentifier("canbeunquoted")
84 | }
85 | }
86 |
87 | func BenchmarkValidIdentifierReserved(b *testing.B) {
88 | for i := 0; i < b.N; i++ {
89 | validIdentifier("function")
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "encoding/json"
7 | "flag"
8 | "fmt"
9 | "io"
10 | "os"
11 | "sort"
12 | "strings"
13 |
14 | "github.com/fatih/color"
15 | "github.com/mattn/go-colorable"
16 | "github.com/nwidger/jsoncolor"
17 | "github.com/pkg/errors"
18 | )
19 |
20 | // Exit codes
21 | const (
22 | exitOK = iota
23 | exitOpenFile
24 | exitReadInput
25 | exitFormStatements
26 | exitFetchURL
27 | exitParseStatements
28 | exitJSONEncode
29 | )
30 |
31 | // Option bitfields
32 | const (
33 | optMonochrome = 1 << iota
34 | optNoSort
35 | optJSON
36 | )
37 |
38 | // Output colors
39 | var (
40 | strColor = color.New(color.FgYellow)
41 | braceColor = color.New(color.FgMagenta)
42 | bareColor = color.New(color.FgBlue, color.Bold)
43 | numColor = color.New(color.FgRed)
44 | boolColor = color.New(color.FgCyan)
45 | )
46 |
47 | // gronVersion stores the current gron version, set at build
48 | // time with the ldflags -X option
49 | var gronVersion = "dev"
50 |
51 | // Default value that is set if proxy/noproxy variables is not specified.
52 | // Cannot be an empty string, because an empty string explicitly deactivates the
53 | // proxy.
54 | var undefinedProxy = "-"
55 |
56 | func init() {
57 | flag.Usage = func() {
58 | h := "Transform JSON (from a file, URL, or stdin) into discrete assignments to make it greppable\n\n"
59 |
60 | h += "Usage:\n"
61 | h += " gron [OPTIONS] [FILE|URL|-]\n\n"
62 |
63 | h += "Options:\n"
64 | h += " -u, --ungron Reverse the operation (turn assignments back into JSON)\n"
65 | h += " -v, --values Print just the values of provided assignments\n"
66 | h += " -c, --colorize Colorize output (default on tty)\n"
67 | h += " -m, --monochrome Monochrome (don't colorize output)\n"
68 | h += " -s, --stream Treat each line of input as a separate JSON object\n"
69 | h += " -k, --insecure Disable certificate validation\n"
70 | h += " -x, --proxy Set proxy configuration\n"
71 | h += " --noproxy Comma-separated list of hosts for which not to use a proxy, if one is specified.\n"
72 | h += " -j, --json Represent gron data as JSON stream\n"
73 | h += " --no-sort Don't sort output (faster)\n"
74 | h += " --version Print version information\n\n"
75 |
76 | h += "Exit Codes:\n"
77 | h += fmt.Sprintf(" %d\t%s\n", exitOK, "OK")
78 | h += fmt.Sprintf(" %d\t%s\n", exitOpenFile, "Failed to open file")
79 | h += fmt.Sprintf(" %d\t%s\n", exitReadInput, "Failed to read input")
80 | h += fmt.Sprintf(" %d\t%s\n", exitFormStatements, "Failed to form statements")
81 | h += fmt.Sprintf(" %d\t%s\n", exitFetchURL, "Failed to fetch URL")
82 | h += fmt.Sprintf(" %d\t%s\n", exitParseStatements, "Failed to parse statements")
83 | h += fmt.Sprintf(" %d\t%s\n", exitJSONEncode, "Failed to encode JSON")
84 | h += "\n"
85 |
86 | h += "Examples:\n"
87 | h += " gron /tmp/apiresponse.json\n"
88 | h += " gron http://jsonplaceholder.typicode.com/users/1 \n"
89 | h += " curl -s http://jsonplaceholder.typicode.com/users/1 | gron\n"
90 | h += " gron http://jsonplaceholder.typicode.com/users/1 | grep company | gron --ungron\n"
91 |
92 | fmt.Fprint(os.Stderr, h)
93 | }
94 | }
95 |
96 | func main() {
97 | var (
98 | ungronFlag bool
99 | colorizeFlag bool
100 | monochromeFlag bool
101 | streamFlag bool
102 | noSortFlag bool
103 | versionFlag bool
104 | insecureFlag bool
105 | jsonFlag bool
106 | valuesFlag bool
107 | proxyURL string
108 | noProxy string
109 | )
110 |
111 | flag.BoolVar(&ungronFlag, "ungron", false, "")
112 | flag.BoolVar(&ungronFlag, "u", false, "")
113 | flag.BoolVar(&colorizeFlag, "colorize", false, "")
114 | flag.BoolVar(&colorizeFlag, "c", false, "")
115 | flag.BoolVar(&monochromeFlag, "monochrome", false, "")
116 | flag.BoolVar(&monochromeFlag, "m", false, "")
117 | flag.BoolVar(&streamFlag, "s", false, "")
118 | flag.BoolVar(&streamFlag, "stream", false, "")
119 | flag.BoolVar(&noSortFlag, "no-sort", false, "")
120 | flag.BoolVar(&versionFlag, "version", false, "")
121 | flag.BoolVar(&insecureFlag, "k", false, "")
122 | flag.BoolVar(&insecureFlag, "insecure", false, "")
123 | flag.BoolVar(&jsonFlag, "j", false, "")
124 | flag.BoolVar(&jsonFlag, "json", false, "")
125 | flag.BoolVar(&valuesFlag, "values", false, "")
126 | flag.BoolVar(&valuesFlag, "value", false, "")
127 | flag.BoolVar(&valuesFlag, "v", false, "")
128 | flag.StringVar(&proxyURL, "x", undefinedProxy, "")
129 | flag.StringVar(&proxyURL, "proxy", undefinedProxy, "")
130 | flag.StringVar(&noProxy, "noproxy", undefinedProxy, "")
131 |
132 | flag.Parse()
133 |
134 | // Print version information
135 | if versionFlag {
136 | fmt.Printf("gron version %s\n", gronVersion)
137 | os.Exit(exitOK)
138 | }
139 |
140 | // If executed as 'ungron' set the --ungron flag
141 | if strings.HasSuffix(os.Args[0], "ungron") {
142 | ungronFlag = true
143 | }
144 |
145 | // Determine what the program's input should be:
146 | // file, HTTP URL or stdin
147 | var rawInput io.Reader
148 | filename := flag.Arg(0)
149 | if filename == "" || filename == "-" {
150 | rawInput = os.Stdin
151 | } else if validURL(filename) {
152 | r, err := getURL(filename, insecureFlag, proxyURL, noProxy)
153 | if err != nil {
154 | fatal(exitFetchURL, err)
155 | }
156 | rawInput = r
157 | } else {
158 | r, err := os.Open(filename)
159 | if err != nil {
160 | fatal(exitOpenFile, err)
161 | }
162 | rawInput = r
163 | }
164 |
165 | var opts int
166 | // The monochrome option should be forced if the output isn't a terminal
167 | // to avoid doing unnecessary work calling the color functions
168 | switch {
169 | case colorizeFlag:
170 | color.NoColor = false
171 | case monochromeFlag || color.NoColor:
172 | opts = opts | optMonochrome
173 | }
174 | if noSortFlag {
175 | opts = opts | optNoSort
176 | }
177 | if jsonFlag {
178 | opts = opts | optJSON
179 | }
180 |
181 | // Pick the appropriate action: gron, ungron, gronValues, or gronStream
182 | var a actionFn = gron
183 | if ungronFlag {
184 | a = ungron
185 | } else if valuesFlag {
186 | a = gronValues
187 | } else if streamFlag {
188 | a = gronStream
189 | }
190 | exitCode, err := a(rawInput, colorable.NewColorableStdout(), opts)
191 |
192 | if exitCode != exitOK {
193 | fatal(exitCode, err)
194 | }
195 |
196 | os.Exit(exitOK)
197 | }
198 |
199 | // an actionFn represents a main action of the program, it accepts
200 | // an input, output and a bitfield of options; returning an exit
201 | // code and any error that occurred
202 | type actionFn func(io.Reader, io.Writer, int) (int, error)
203 |
204 | // gron is the default action. Given JSON as the input it returns a list
205 | // of assignment statements. Possible options are optNoSort and optMonochrome
206 | func gron(r io.Reader, w io.Writer, opts int) (int, error) {
207 | var err error
208 |
209 | var conv statementconv
210 | if opts&optMonochrome > 0 {
211 | conv = statementToString
212 | } else {
213 | conv = statementToColorString
214 | }
215 |
216 | ss, err := statementsFromJSON(r, statement{{"json", typBare}})
217 | if err != nil {
218 | goto out
219 | }
220 |
221 | // Go's maps do not have well-defined ordering, but we want a consistent
222 | // output for a given input, so we must sort the statements
223 | if opts&optNoSort == 0 {
224 | sort.Sort(ss)
225 | }
226 |
227 | for _, s := range ss {
228 | if opts&optJSON > 0 {
229 | s, err = s.jsonify()
230 | if err != nil {
231 | goto out
232 | }
233 | }
234 | fmt.Fprintln(w, conv(s))
235 | }
236 |
237 | out:
238 | if err != nil {
239 | return exitFormStatements, fmt.Errorf("failed to form statements: %s", err)
240 | }
241 | return exitOK, nil
242 | }
243 |
244 | // gronStream is like the gron action, but it treats the input as one
245 | // JSON object per line. There's a bit of code duplication from the
246 | // gron action, but it'd be fairly messy to combine the two actions
247 | func gronStream(r io.Reader, w io.Writer, opts int) (int, error) {
248 | var err error
249 | errstr := "failed to form statements"
250 | var i int
251 | var sc *bufio.Scanner
252 | var buf []byte
253 |
254 | var conv func(s statement) string
255 | if opts&optMonochrome > 0 {
256 | conv = statementToString
257 | } else {
258 | conv = statementToColorString
259 | }
260 |
261 | // Helper function to make the prefix statements for each line
262 | makePrefix := func(index int) statement {
263 | return statement{
264 | {"json", typBare},
265 | {"[", typLBrace},
266 | {fmt.Sprintf("%d", index), typNumericKey},
267 | {"]", typRBrace},
268 | }
269 | }
270 |
271 | // The first line of output needs to establish that the top-level
272 | // thing is actually an array...
273 | top := statement{
274 | {"json", typBare},
275 | {"=", typEquals},
276 | {"[]", typEmptyArray},
277 | {";", typSemi},
278 | }
279 |
280 | if opts&optJSON > 0 {
281 | top, err = top.jsonify()
282 | if err != nil {
283 | goto out
284 | }
285 | }
286 |
287 | fmt.Fprintln(w, conv(top))
288 |
289 | // Read the input line by line
290 | sc = bufio.NewScanner(r)
291 | buf = make([]byte, 0, 64*1024)
292 | sc.Buffer(buf, 1024*1024)
293 | i = 0
294 | for sc.Scan() {
295 |
296 | line := bytes.NewBuffer(sc.Bytes())
297 |
298 | var ss statements
299 | ss, err = statementsFromJSON(line, makePrefix(i))
300 | i++
301 | if err != nil {
302 | goto out
303 | }
304 |
305 | // Go's maps do not have well-defined ordering, but we want a consistent
306 | // output for a given input, so we must sort the statements
307 | if opts&optNoSort == 0 {
308 | sort.Sort(ss)
309 | }
310 |
311 | for _, s := range ss {
312 | if opts&optJSON > 0 {
313 | s, err = s.jsonify()
314 | if err != nil {
315 | goto out
316 | }
317 |
318 | }
319 | fmt.Fprintln(w, conv(s))
320 | }
321 | }
322 | if err = sc.Err(); err != nil {
323 | errstr = "error reading multiline input: %s"
324 | }
325 |
326 | out:
327 | if err != nil {
328 | return exitFormStatements, fmt.Errorf(errstr+": %s", err)
329 | }
330 | return exitOK, nil
331 |
332 | }
333 |
334 | // ungron is the reverse of gron. Given assignment statements as input,
335 | // it returns JSON. The only option is optMonochrome
336 | func ungron(r io.Reader, w io.Writer, opts int) (int, error) {
337 | scanner := bufio.NewScanner(r)
338 | var maker statementmaker
339 |
340 | // Allow larger internal buffer of the scanner (min: 64KiB ~ max: 1MiB)
341 | scanner.Buffer(make([]byte, 64*1024), 1024*1024)
342 |
343 | if opts&optJSON > 0 {
344 | maker = statementFromJSONSpec
345 | } else {
346 | maker = statementFromStringMaker
347 | }
348 |
349 | // Make a list of statements from the input
350 | var ss statements
351 | for scanner.Scan() {
352 | s, err := maker(scanner.Text())
353 | if err != nil {
354 | return exitParseStatements, err
355 | }
356 | ss.add(s)
357 | }
358 | if err := scanner.Err(); err != nil {
359 | return exitReadInput, fmt.Errorf("failed to read input statements")
360 | }
361 |
362 | // turn the statements into a single merged interface{} type
363 | merged, err := ss.toInterface()
364 | if err != nil {
365 | return exitParseStatements, err
366 | }
367 |
368 | // If there's only one top level key and it's "json", make that the top level thing
369 | mergedMap, ok := merged.(map[string]interface{})
370 | if ok {
371 | if len(mergedMap) == 1 {
372 | if _, exists := mergedMap["json"]; exists {
373 | merged = mergedMap["json"]
374 | }
375 | }
376 | }
377 |
378 | // Marshal the output into JSON to display to the user
379 | out := &bytes.Buffer{}
380 | enc := json.NewEncoder(out)
381 | enc.SetIndent("", " ")
382 | enc.SetEscapeHTML(false)
383 | err = enc.Encode(merged)
384 | if err != nil {
385 | return exitJSONEncode, errors.Wrap(err, "failed to convert statements to JSON")
386 | }
387 | j := out.Bytes()
388 |
389 | // If the output isn't monochrome, add color to the JSON
390 | if opts&optMonochrome == 0 {
391 | c, err := colorizeJSON(j)
392 |
393 | // If we failed to colorize the JSON for whatever reason,
394 | // we'll just fall back to monochrome output, otherwise
395 | // replace the monochrome JSON with glorious technicolor
396 | if err == nil {
397 | j = c
398 | }
399 | }
400 |
401 | // For whatever reason, the monochrome version of the JSON
402 | // has a trailing newline character, but the colorized version
403 | // does not. Strip the whitespace so that neither has the newline
404 | // character on the end, and then we'll add a newline in the
405 | // Fprintf below
406 | j = bytes.TrimSpace(j)
407 |
408 | fmt.Fprintf(w, "%s\n", j)
409 |
410 | return exitOK, nil
411 | }
412 |
413 | // gronValues prints just the scalar values from some input gron statements
414 | // without any quotes or anything of that sort; a bit like jq -r
415 | // e.g. json[0].user.name = "Sam"; -> Sam
416 | func gronValues(r io.Reader, w io.Writer, opts int) (int, error) {
417 | scanner := bufio.NewScanner(os.Stdin)
418 |
419 | for scanner.Scan() {
420 | s := statementFromString(scanner.Text())
421 |
422 | if len(s) == 0 {
423 | return exitParseStatements, fmt.Errorf("failed to parse '%s' as gron statement", scanner.Text())
424 | }
425 |
426 | // strip off the leading 'json' bare key
427 | if s[0].typ == typBare && s[0].text == "json" {
428 | s = s[1:]
429 | }
430 |
431 | // strip off the leading dots
432 | if s[0].typ == typDot || s[0].typ == typLBrace {
433 | s = s[1:]
434 | }
435 |
436 | for _, t := range s {
437 | switch t.typ {
438 | case typString:
439 | var text string
440 | err := json.Unmarshal([]byte(t.text), &text)
441 | if err != nil {
442 | // just swallow errors and try to continue
443 | continue
444 | }
445 | fmt.Println(text)
446 |
447 | case typNumber, typTrue, typFalse, typNull:
448 | fmt.Println(t.text)
449 |
450 | default:
451 | // Nothing
452 | }
453 | }
454 | }
455 |
456 | return exitOK, nil
457 | }
458 |
459 | func colorizeJSON(src []byte) ([]byte, error) {
460 | out := &bytes.Buffer{}
461 | f := jsoncolor.NewFormatter()
462 |
463 | f.StringColor = strColor
464 | f.ObjectColor = braceColor
465 | f.ArrayColor = braceColor
466 | f.FieldColor = bareColor
467 | f.NumberColor = numColor
468 | f.TrueColor = boolColor
469 | f.FalseColor = boolColor
470 | f.NullColor = boolColor
471 |
472 | err := f.Format(out, src)
473 | if err != nil {
474 | return out.Bytes(), err
475 | }
476 | return out.Bytes(), nil
477 | }
478 |
479 | func fatal(code int, err error) {
480 | fmt.Fprintf(os.Stderr, "%s\n", err)
481 | os.Exit(code)
482 | }
483 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io/ioutil"
7 | "os"
8 | "reflect"
9 | "testing"
10 | )
11 |
12 | func TestGron(t *testing.T) {
13 | cases := []struct {
14 | inFile string
15 | outFile string
16 | }{
17 | {"testdata/one.json", "testdata/one.gron"},
18 | {"testdata/two.json", "testdata/two.gron"},
19 | {"testdata/three.json", "testdata/three.gron"},
20 | {"testdata/github.json", "testdata/github.gron"},
21 | }
22 |
23 | for _, c := range cases {
24 | in, err := os.Open(c.inFile)
25 | if err != nil {
26 | t.Fatalf("failed to open input file: %s", err)
27 | }
28 |
29 | want, err := ioutil.ReadFile(c.outFile)
30 | if err != nil {
31 | t.Fatalf("failed to open want file: %s", err)
32 | }
33 |
34 | out := &bytes.Buffer{}
35 | code, err := gron(in, out, optMonochrome)
36 |
37 | if code != exitOK {
38 | t.Errorf("want exitOK; have %d", code)
39 | }
40 | if err != nil {
41 | t.Errorf("want nil error; have %s", err)
42 | }
43 |
44 | if !reflect.DeepEqual(want, out.Bytes()) {
45 | t.Logf("want: %s", want)
46 | t.Logf("have: %s", out.Bytes())
47 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile)
48 | }
49 | }
50 |
51 | }
52 |
53 | func TestGronStream(t *testing.T) {
54 | cases := []struct {
55 | inFile string
56 | outFile string
57 | }{
58 | {"testdata/stream.json", "testdata/stream.gron"},
59 | {"testdata/scalar-stream.json", "testdata/scalar-stream.gron"},
60 | }
61 |
62 | for _, c := range cases {
63 | in, err := os.Open(c.inFile)
64 | if err != nil {
65 | t.Fatalf("failed to open input file: %s", err)
66 | }
67 |
68 | want, err := ioutil.ReadFile(c.outFile)
69 | if err != nil {
70 | t.Fatalf("failed to open want file: %s", err)
71 | }
72 |
73 | out := &bytes.Buffer{}
74 | code, err := gronStream(in, out, optMonochrome)
75 |
76 | if code != exitOK {
77 | t.Errorf("want exitOK; have %d", code)
78 | }
79 | if err != nil {
80 | t.Errorf("want nil error; have %s", err)
81 | }
82 |
83 | if !reflect.DeepEqual(want, out.Bytes()) {
84 | t.Logf("want: %s", want)
85 | t.Logf("have: %s", out.Bytes())
86 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile)
87 | }
88 | }
89 |
90 | }
91 |
92 | func TestLargeGronStream(t *testing.T) {
93 | cases := []struct {
94 | inFile string
95 | outFile string
96 | }{
97 | {"testdata/long-stream.json", "testdata/long-stream.gron"},
98 | }
99 |
100 | for _, c := range cases {
101 | in, err := os.Open(c.inFile)
102 | if err != nil {
103 | t.Fatalf("failed to open input file: %s", err)
104 | }
105 |
106 | want, err := ioutil.ReadFile(c.outFile)
107 | if err != nil {
108 | t.Fatalf("failed to open want file: %s", err)
109 | }
110 |
111 | out := &bytes.Buffer{}
112 | code, err := gronStream(in, out, optMonochrome)
113 |
114 | if code != exitOK {
115 | t.Errorf("want exitOK; have %d", code)
116 | }
117 | if err != nil {
118 | t.Errorf("want nil error; have %s", err)
119 | }
120 |
121 | if !reflect.DeepEqual(want, out.Bytes()) {
122 | t.Logf("want: %s", want)
123 | t.Logf("have: %s", out.Bytes())
124 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile)
125 | }
126 | }
127 |
128 | }
129 |
130 | func TestUngron(t *testing.T) {
131 | cases := []struct {
132 | inFile string
133 | outFile string
134 | }{
135 | {"testdata/one.gron", "testdata/one.json"},
136 | {"testdata/two.gron", "testdata/two.json"},
137 | {"testdata/three.gron", "testdata/three.json"},
138 | {"testdata/grep-separators.gron", "testdata/grep-separators.json"},
139 | {"testdata/github.gron", "testdata/github.json"},
140 | {"testdata/large-line.gron", "testdata/large-line.json"},
141 | }
142 |
143 | for _, c := range cases {
144 | wantF, err := ioutil.ReadFile(c.outFile)
145 | if err != nil {
146 | t.Fatalf("failed to open want file: %s", err)
147 | }
148 |
149 | var want interface{}
150 | err = json.Unmarshal(wantF, &want)
151 | if err != nil {
152 | t.Fatalf("failed to unmarshal JSON from want file: %s", err)
153 | }
154 |
155 | in, err := os.Open(c.inFile)
156 | if err != nil {
157 | t.Fatalf("failed to open input file: %s", err)
158 | }
159 |
160 | out := &bytes.Buffer{}
161 | code, err := ungron(in, out, optMonochrome)
162 |
163 | if code != exitOK {
164 | t.Errorf("want exitOK; have %d", code)
165 | }
166 | if err != nil {
167 | t.Errorf("want nil error; have %s", err)
168 | }
169 |
170 | var have interface{}
171 | err = json.Unmarshal(out.Bytes(), &have)
172 | if err != nil {
173 | t.Fatalf("failed to unmarshal JSON from ungron output: %s", err)
174 | }
175 |
176 | if !reflect.DeepEqual(want, have) {
177 | t.Logf("want: %#v", want)
178 | t.Logf("have: %#v", have)
179 | t.Errorf("ungronned %s does not match %s", c.inFile, c.outFile)
180 | }
181 |
182 | }
183 | }
184 |
185 | func TestGronJ(t *testing.T) {
186 | cases := []struct {
187 | inFile string
188 | outFile string
189 | }{
190 | {"testdata/one.json", "testdata/one.jgron"},
191 | {"testdata/two.json", "testdata/two.jgron"},
192 | {"testdata/three.json", "testdata/three.jgron"},
193 | {"testdata/github.json", "testdata/github.jgron"},
194 | }
195 |
196 | for _, c := range cases {
197 | in, err := os.Open(c.inFile)
198 | if err != nil {
199 | t.Fatalf("failed to open input file: %s", err)
200 | }
201 |
202 | want, err := ioutil.ReadFile(c.outFile)
203 | if err != nil {
204 | t.Fatalf("failed to open want file: %s", err)
205 | }
206 |
207 | out := &bytes.Buffer{}
208 | code, err := gron(in, out, optMonochrome|optJSON)
209 |
210 | if code != exitOK {
211 | t.Errorf("want exitOK; have %d", code)
212 | }
213 | if err != nil {
214 | t.Errorf("want nil error; have %s", err)
215 | }
216 |
217 | if !reflect.DeepEqual(want, out.Bytes()) {
218 | t.Logf("want: %s", want)
219 | t.Logf("have: %s", out.Bytes())
220 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile)
221 | }
222 | }
223 |
224 | }
225 |
226 | func TestGronStreamJ(t *testing.T) {
227 | cases := []struct {
228 | inFile string
229 | outFile string
230 | }{
231 | {"testdata/stream.json", "testdata/stream.jgron"},
232 | {"testdata/scalar-stream.json", "testdata/scalar-stream.jgron"},
233 | }
234 |
235 | for _, c := range cases {
236 | in, err := os.Open(c.inFile)
237 | if err != nil {
238 | t.Fatalf("failed to open input file: %s", err)
239 | }
240 |
241 | want, err := ioutil.ReadFile(c.outFile)
242 | if err != nil {
243 | t.Fatalf("failed to open want file: %s", err)
244 | }
245 |
246 | out := &bytes.Buffer{}
247 | code, err := gronStream(in, out, optMonochrome|optJSON)
248 |
249 | if code != exitOK {
250 | t.Errorf("want exitOK; have %d", code)
251 | }
252 | if err != nil {
253 | t.Errorf("want nil error; have %s", err)
254 | }
255 |
256 | if !reflect.DeepEqual(want, out.Bytes()) {
257 | t.Logf("want: %s", want)
258 | t.Logf("have: %s", out.Bytes())
259 | t.Errorf("gronned %s does not match %s", c.inFile, c.outFile)
260 | }
261 | }
262 |
263 | }
264 |
265 | func TestUngronJ(t *testing.T) {
266 | cases := []struct {
267 | inFile string
268 | outFile string
269 | }{
270 | {"testdata/one.jgron", "testdata/one.json"},
271 | {"testdata/two.jgron", "testdata/two.json"},
272 | {"testdata/three.jgron", "testdata/three.json"},
273 | {"testdata/github.jgron", "testdata/github.json"},
274 | }
275 |
276 | for _, c := range cases {
277 | wantF, err := ioutil.ReadFile(c.outFile)
278 | if err != nil {
279 | t.Fatalf("failed to open want file: %s", err)
280 | }
281 |
282 | var want interface{}
283 | err = json.Unmarshal(wantF, &want)
284 | if err != nil {
285 | t.Fatalf("failed to unmarshal JSON from want file: %s", err)
286 | }
287 |
288 | in, err := os.Open(c.inFile)
289 | if err != nil {
290 | t.Fatalf("failed to open input file: %s", err)
291 | }
292 |
293 | out := &bytes.Buffer{}
294 | code, err := ungron(in, out, optMonochrome|optJSON)
295 |
296 | if code != exitOK {
297 | t.Errorf("want exitOK; have %d", code)
298 | }
299 | if err != nil {
300 | t.Errorf("want nil error; have %s", err)
301 | }
302 |
303 | var have interface{}
304 | err = json.Unmarshal(out.Bytes(), &have)
305 | if err != nil {
306 | t.Fatalf("failed to unmarshal JSON from ungron output: %s", err)
307 | }
308 |
309 | if !reflect.DeepEqual(want, have) {
310 | t.Logf("want: %#v", want)
311 | t.Logf("have: %#v", have)
312 | t.Errorf("ungronned %s does not match %s", c.inFile, c.outFile)
313 | }
314 |
315 | }
316 | }
317 |
318 | func BenchmarkBigJSON(b *testing.B) {
319 | in, err := os.Open("testdata/big.json")
320 | if err != nil {
321 | b.Fatalf("failed to open test data file: %s", err)
322 | }
323 |
324 | for i := 0; i < b.N; i++ {
325 | out := &bytes.Buffer{}
326 | _, err = in.Seek(0, 0)
327 | if err != nil {
328 | b.Fatalf("failed to rewind input: %s", err)
329 | }
330 |
331 | _, err := gron(in, out, optMonochrome|optNoSort)
332 | if err != nil {
333 | b.Fatalf("failed to gron: %s", err)
334 | }
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/original-gron.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | $v){
94 | $k = json_encode($k);
95 | printSruct($v, "{$prefix}[{$k}]");
96 | }
97 | }
98 |
99 | exit(0);
100 |
--------------------------------------------------------------------------------
/script/example:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | PROJDIR=$(cd `dirname $0`/.. && pwd)
3 | cd $PROJDIR
4 | go build
5 | ./gron testdata/one.json
6 |
--------------------------------------------------------------------------------
/script/lint:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | PROJDIR=$(cd `dirname $0`/.. && pwd)
3 | cd ${PROJDIR}
4 |
5 | which gometalinter > /dev/null
6 |
7 | if [ $? -ne 0 ]; then
8 | echo "You don't have gometalinter. Installing it..."
9 | go get github.com/alecthomas/gometalinter
10 | gometalinter --install
11 | fi
12 |
13 | # dupl is disabled because it has a habbit of identifying tests as duplicated code.
14 | # in its defence: it's right. gocyclo is disabled because I'm a terrible programmer.
15 | # gas is disabled because it doesn't like InsecureSkipVerify set to true for HTTP
16 | # requests - but we only do that if the user asks for it.
17 | gometalinter --disable=gocyclo --disable=dupl --enable=goimports --disable=gas
18 |
--------------------------------------------------------------------------------
/script/precommit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | PROJDIR=$(cd `dirname $0`/.. && pwd)
4 | cd ${PROJDIR}
5 |
6 | ./script/test
7 |
--------------------------------------------------------------------------------
/script/profile:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | PROJDIR=$(cd `dirname $0`/.. && pwd)
4 | cd ${PROJDIR}
5 |
6 | PAT="."
7 | if [ ! -z "${1}" ]; then
8 | PAT="${1}"
9 | fi
10 |
11 | go test -bench ${PAT} -benchmem -cpuprofile cpu.out
12 | go tool pprof gron.test cpu.out
13 |
--------------------------------------------------------------------------------
/script/release:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | PROJDIR=$(cd `dirname $0`/.. && pwd)
3 |
4 | VERSION="${1}"
5 | TAG="v${VERSION}"
6 | USER="tomnomnom"
7 | REPO="gron"
8 | BINARY="${REPO}"
9 |
10 | if [[ -z "${VERSION}" ]]; then
11 | echo "Usage: ${0} "
12 | exit 1
13 | fi
14 |
15 | if [[ -z "${GITHUB_TOKEN}" ]]; then
16 | echo "You forgot to set your GITHUB_TOKEN"
17 | exit 2
18 | fi
19 |
20 | cd ${PROJDIR}
21 |
22 | # Run the tests
23 | go test
24 | if [ $? -ne 0 ]; then
25 | echo "Tests failed. Aborting."
26 | exit 3
27 | fi
28 |
29 | FILELIST=""
30 |
31 | for ARCH in "amd64" "386" "arm64" "arm"; do
32 | for OS in "darwin" "linux" "windows" "freebsd"; do
33 |
34 | if [[ "${OS}" == "darwin" && ("${ARCH}" == "386" || "${ARCH}" == "arm") ]]; then
35 | continue
36 | fi
37 |
38 | BINFILE="${BINARY}"
39 |
40 | if [[ "${OS}" == "windows" ]]; then
41 | BINFILE="${BINFILE}.exe"
42 | fi
43 |
44 | rm -f ${BINFILE}
45 |
46 | GOOS=${OS} GOARCH=${ARCH} go build -ldflags "-X main.gronVersion=${VERSION}" github.com/${USER}/${REPO}
47 |
48 | if [[ "${OS}" == "windows" ]]; then
49 | ARCHIVE="${BINARY}-${OS}-${ARCH}-${VERSION}.zip"
50 | zip ${ARCHIVE} ${BINFILE}
51 | rm ${BINFILE}
52 | else
53 | ARCHIVE="${BINARY}-${OS}-${ARCH}-${VERSION}.tgz"
54 | tar --create --gzip --file=${ARCHIVE} ${BINFILE}
55 | fi
56 |
57 | FILELIST="${FILELIST} ${PROJDIR}/${ARCHIVE}"
58 | done
59 | done
60 |
61 | gh release create ${TAG} ${FILELIST}
62 | rm ${FILELIST}
63 |
64 |
--------------------------------------------------------------------------------
/script/test:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | PROJDIR=$(cd `dirname $0`/.. && pwd)
3 | cd ${PROJDIR}
4 |
5 | go test
6 |
--------------------------------------------------------------------------------
/statements.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "reflect"
8 | "strconv"
9 | "strings"
10 |
11 | "github.com/pkg/errors"
12 | )
13 |
14 | // A statement is a slice of tokens representing an assignment statement.
15 | // An assignment statement is something like:
16 | //
17 | // json.city = "Leeds";
18 | //
19 | // Where 'json', '.', 'city', '=', '"Leeds"' and ';' are discrete tokens.
20 | // Statements are stored as tokens to make sorting more efficient, and so
21 | // that the same type can easily be used when gronning and ungronning.
22 | type statement []token
23 |
24 | // String returns the string form of a statement rather than the
25 | // underlying slice of tokens
26 | func (s statement) String() string {
27 | out := make([]string, 0, len(s)+2)
28 | for _, t := range s {
29 | out = append(out, t.format())
30 | }
31 | return strings.Join(out, "")
32 | }
33 |
34 | // colorString returns the string form of a statement with ASCII color codes
35 | func (s statement) colorString() string {
36 | out := make([]string, 0, len(s)+2)
37 | for _, t := range s {
38 | out = append(out, t.formatColor())
39 | }
40 | return strings.Join(out, "")
41 | }
42 |
43 | // a statementconv converts a statement to string
44 | type statementconv func(s statement) string
45 |
46 | // statementconv variant of statement.String
47 | func statementToString(s statement) string {
48 | return s.String()
49 | }
50 |
51 | // statementconv variant of statement.colorString
52 | func statementToColorString(s statement) string {
53 | return s.colorString()
54 | }
55 |
56 | // withBare returns a copy of a statement with a new bare
57 | // word token appended to it
58 | func (s statement) withBare(k string) statement {
59 | new := make(statement, len(s), len(s)+2)
60 | copy(new, s)
61 | return append(
62 | new,
63 | token{".", typDot},
64 | token{k, typBare},
65 | )
66 | }
67 |
68 | // jsonify converts an assignment statement to a JSON representation
69 | func (s statement) jsonify() (statement, error) {
70 | // If m is the number of keys occurring in the left hand side
71 | // of s, then len(s) is in between 2*m+4 and 3*m+4. The resultant
72 | // statement j (carrying the JSON representation) is always 2*m+5
73 | // long. So len(s)+1 ≥ 2*m+5 = len(j). Therefore an initaial
74 | // allocation of j with capacity len(s)+1 will allow us to carry
75 | // through without reallocation.
76 | j := make(statement, 0, len(s)+1)
77 | if len(s) < 4 || s[0].typ != typBare || s[len(s)-3].typ != typEquals ||
78 | s[len(s)-1].typ != typSemi {
79 | return nil, errors.New("non-assignment statement")
80 | }
81 |
82 | j = append(j, token{"[", typLBrace})
83 | j = append(j, token{"[", typLBrace})
84 | for _, t := range s[1 : len(s)-3] {
85 | switch t.typ {
86 | case typNumericKey, typQuotedKey:
87 | j = append(j, t)
88 | j = append(j, token{",", typComma})
89 | case typBare:
90 | j = append(j, token{quoteString(t.text), typQuotedKey})
91 | j = append(j, token{",", typComma})
92 | }
93 | }
94 | if j[len(j)-1].typ == typComma {
95 | j = j[:len(j)-1]
96 | }
97 | j = append(j, token{"]", typLBrace})
98 | j = append(j, token{",", typComma})
99 | j = append(j, s[len(s)-2])
100 | j = append(j, token{"]", typLBrace})
101 |
102 | return j, nil
103 | }
104 |
105 | // withQuotedKey returns a copy of a statement with a new
106 | // quoted key token appended to it
107 | func (s statement) withQuotedKey(k string) statement {
108 | new := make(statement, len(s), len(s)+3)
109 | copy(new, s)
110 | return append(
111 | new,
112 | token{"[", typLBrace},
113 | token{quoteString(k), typQuotedKey},
114 | token{"]", typRBrace},
115 | )
116 | }
117 |
118 | // withNumericKey returns a copy of a statement with a new
119 | // numeric key token appended to it
120 | func (s statement) withNumericKey(k int) statement {
121 | new := make(statement, len(s), len(s)+3)
122 | copy(new, s)
123 | return append(
124 | new,
125 | token{"[", typLBrace},
126 | token{strconv.Itoa(k), typNumericKey},
127 | token{"]", typRBrace},
128 | )
129 | }
130 |
131 | // statements is a list of assignment statements.
132 | // E.g statement: json.foo = "bar";
133 | type statements []statement
134 |
135 | // addWithValue takes a statement representing a path, copies it,
136 | // adds a value token to the end of the statement and appends
137 | // the new statement to the list of statements
138 | func (ss *statements) addWithValue(path statement, value token) {
139 | s := make(statement, len(path), len(path)+3)
140 | copy(s, path)
141 | s = append(s, token{"=", typEquals}, value, token{";", typSemi})
142 | *ss = append(*ss, s)
143 | }
144 |
145 | // add appends a new complete statement to list of statements
146 | func (ss *statements) add(s statement) {
147 | *ss = append(*ss, s)
148 | }
149 |
150 | // Len returns the number of statements for sort.Sort
151 | func (ss statements) Len() int {
152 | return len(ss)
153 | }
154 |
155 | // Swap swaps two statements for sort.Sort
156 | func (ss statements) Swap(i, j int) {
157 | ss[i], ss[j] = ss[j], ss[i]
158 | }
159 |
160 | // a statementmaker is a function that makes a statement
161 | // from string
162 | type statementmaker func(str string) (statement, error)
163 |
164 | // statementFromString takes statement string, lexes it and returns
165 | // the corresponding statement
166 | func statementFromString(str string) statement {
167 | l := newLexer(str)
168 | s := l.lex()
169 | return s
170 | }
171 |
172 | // statementmaker variant of statementFromString
173 | func statementFromStringMaker(str string) (statement, error) {
174 | return statementFromString(str), nil
175 | }
176 |
177 | // statementFromJson returns statement encoded by
178 | // JSON specification
179 | func statementFromJSONSpec(str string) (statement, error) {
180 | var a []interface{}
181 | var ok bool
182 | var v interface{}
183 | var s statement
184 | var t tokenTyp
185 | var nstr string
186 | var nbuf []byte
187 |
188 | err := json.Unmarshal([]byte(str), &a)
189 | if err != nil {
190 | return nil, err
191 | }
192 | if len(a) != 2 {
193 | goto out
194 | }
195 |
196 | v = a[1]
197 | a, ok = a[0].([]interface{})
198 | if !ok {
199 | goto out
200 | }
201 |
202 | // We'll append one initial token, then 3 tokens for each element of a,
203 | // then 3 closing tokens, that's alltogether 3*len(a)+4.
204 | s = make(statement, 0, 3*len(a)+4)
205 | s = append(s, token{"json", typBare})
206 | for _, e := range a {
207 | s = append(s, token{"[", typLBrace})
208 | switch e := e.(type) {
209 | case string:
210 | s = append(s, token{quoteString(e), typQuotedKey})
211 | case float64:
212 | nbuf, err = json.Marshal(e)
213 | if err != nil {
214 | return nil, errors.Wrap(err, "JSON internal error")
215 | }
216 | nstr = string(nbuf)
217 | s = append(s, token{nstr, typNumericKey})
218 | default:
219 | ok = false
220 | goto out
221 | }
222 | s = append(s, token{"]", typRBrace})
223 | }
224 |
225 | s = append(s, token{"=", typEquals})
226 |
227 | switch v := v.(type) {
228 | case bool:
229 | if v {
230 | t = typTrue
231 | } else {
232 | t = typFalse
233 | }
234 | case float64:
235 | t = typNumber
236 | case string:
237 | t = typString
238 | case []interface{}:
239 | ok = (len(v) == 0)
240 | if !ok {
241 | goto out
242 | }
243 | t = typEmptyArray
244 | case map[string]interface{}:
245 | ok = (len(v) == 0)
246 | if !ok {
247 | goto out
248 | }
249 | t = typEmptyObject
250 | default:
251 | ok = (v == nil)
252 | if !ok {
253 | goto out
254 | }
255 | t = typNull
256 | }
257 |
258 | nbuf, err = json.Marshal(v)
259 | if err != nil {
260 | return nil, errors.Wrap(err, "JSON internal error")
261 | }
262 | nstr = string(nbuf)
263 | s = append(s, token{nstr, t})
264 |
265 | s = append(s, token{";", typSemi})
266 |
267 | out:
268 | if !ok {
269 | return nil, errors.New("invalid JSON layout")
270 | }
271 | return s, nil
272 | }
273 |
274 | // ungron turns statements into a proper datastructure
275 | func (ss statements) toInterface() (interface{}, error) {
276 |
277 | // Get all the individually parsed statements
278 | var parsed []interface{}
279 | for _, s := range ss {
280 | u, err := ungronTokens(s)
281 |
282 | switch err.(type) {
283 | case nil:
284 | // no problem :)
285 | case errRecoverable:
286 | continue
287 | default:
288 | return nil, errors.Wrapf(err, "ungron failed for `%s`", s)
289 | }
290 |
291 | parsed = append(parsed, u)
292 | }
293 |
294 | if len(parsed) == 0 {
295 | return nil, fmt.Errorf("no statements were parsed")
296 | }
297 |
298 | merged := parsed[0]
299 | for _, p := range parsed[1:] {
300 | m, err := recursiveMerge(merged, p)
301 | if err != nil {
302 | return nil, errors.Wrap(err, "failed to merge statements")
303 | }
304 | merged = m
305 | }
306 | return merged, nil
307 |
308 | }
309 |
310 | // Less compares two statements for sort.Sort
311 | // Implements a natural sort to keep array indexes in order
312 | func (ss statements) Less(a, b int) bool {
313 |
314 | // ss[a] and ss[b] are both slices of tokens. The first
315 | // thing we need to do is find the first token (if any)
316 | // that differs, then we can use that token to decide
317 | // if ss[a] or ss[b] should come first in the sort.
318 | diffIndex := -1
319 | for i := range ss[a] {
320 |
321 | if len(ss[b]) < i+1 {
322 | // b must be shorter than a, so it
323 | // should come first
324 | return false
325 | }
326 |
327 | // The tokens match, so just carry on
328 | if ss[a][i] == ss[b][i] {
329 | continue
330 | }
331 |
332 | // We've found a difference
333 | diffIndex = i
334 | break
335 | }
336 |
337 | // If diffIndex is still -1 then the only difference must be
338 | // that ss[b] is longer than ss[a], so ss[a] should come first
339 | if diffIndex == -1 {
340 | return true
341 | }
342 |
343 | // Get the tokens that differ
344 | ta := ss[a][diffIndex]
345 | tb := ss[b][diffIndex]
346 |
347 | // An equals always comes first
348 | if ta.typ == typEquals {
349 | return true
350 | }
351 | if tb.typ == typEquals {
352 | return false
353 | }
354 |
355 | // If both tokens are numeric keys do an integer comparison
356 | if ta.typ == typNumericKey && tb.typ == typNumericKey {
357 | ia, _ := strconv.Atoi(ta.text)
358 | ib, _ := strconv.Atoi(tb.text)
359 | return ia < ib
360 | }
361 |
362 | // If neither token is a number, just do a string comparison
363 | if ta.typ != typNumber || tb.typ != typNumber {
364 | return ta.text < tb.text
365 | }
366 |
367 | // We have two numbers to compare so turn them into json.Number
368 | // for comparison
369 | na, _ := json.Number(ta.text).Float64()
370 | nb, _ := json.Number(tb.text).Float64()
371 | return na < nb
372 |
373 | }
374 |
375 | // Contains searches the statements for a given statement
376 | // Mostly to make testing things easier
377 | func (ss statements) Contains(search statement) bool {
378 | for _, i := range ss {
379 | if reflect.DeepEqual(i, search) {
380 | return true
381 | }
382 | }
383 | return false
384 | }
385 |
386 | // statementsFromJSON takes an io.Reader containing JSON
387 | // and returns statements or an error on failure
388 | func statementsFromJSON(r io.Reader, prefix statement) (statements, error) {
389 | var top interface{}
390 | d := json.NewDecoder(r)
391 | d.UseNumber()
392 | err := d.Decode(&top)
393 | if err != nil {
394 | return nil, err
395 | }
396 | ss := make(statements, 0, 32)
397 | ss.fill(prefix, top)
398 | return ss, nil
399 | }
400 |
401 | // fill takes a prefix statement and some value and recursively fills
402 | // the statement list using that value
403 | func (ss *statements) fill(prefix statement, v interface{}) {
404 |
405 | // Add a statement for the current prefix and value
406 | ss.addWithValue(prefix, valueTokenFromInterface(v))
407 |
408 | // Recurse into objects and arrays
409 | switch vv := v.(type) {
410 |
411 | case map[string]interface{}:
412 | // It's an object
413 | for k, sub := range vv {
414 | if validIdentifier(k) {
415 | ss.fill(prefix.withBare(k), sub)
416 | } else {
417 | ss.fill(prefix.withQuotedKey(k), sub)
418 | }
419 | }
420 |
421 | case []interface{}:
422 | // It's an array
423 | for k, sub := range vv {
424 | ss.fill(prefix.withNumericKey(k), sub)
425 | }
426 | }
427 |
428 | }
429 |
--------------------------------------------------------------------------------
/statements_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "reflect"
7 | "sort"
8 | "testing"
9 | )
10 |
11 | func statementsFromStringSlice(strs []string) statements {
12 | ss := make(statements, len(strs))
13 | for i, str := range strs {
14 | ss[i] = statementFromString(str)
15 | }
16 | return ss
17 | }
18 |
19 | func TestStatementsSimple(t *testing.T) {
20 |
21 | j := []byte(`{
22 | "dotted": "A dotted value",
23 | "a quoted": "value",
24 | "bool1": true,
25 | "bool2": false,
26 | "anull": null,
27 | "anarr": [1, 1.5],
28 | "anob": {
29 | "foo": "bar"
30 | },
31 | "else": 1,
32 | "id": 66912849,
33 | "": 2
34 | }`)
35 |
36 | ss, err := statementsFromJSON(bytes.NewReader(j), statement{{"json", typBare}})
37 |
38 | if err != nil {
39 | t.Errorf("Want nil error from makeStatementsFromJSON() but got %s", err)
40 | }
41 |
42 | wants := statementsFromStringSlice([]string{
43 | `json = {};`,
44 | `json.dotted = "A dotted value";`,
45 | `json["a quoted"] = "value";`,
46 | `json.bool1 = true;`,
47 | `json.bool2 = false;`,
48 | `json.anull = null;`,
49 | `json.anarr = [];`,
50 | `json.anarr[0] = 1;`,
51 | `json.anarr[1] = 1.5;`,
52 | `json.anob = {};`,
53 | `json.anob.foo = "bar";`,
54 | `json["else"] = 1;`,
55 | `json.id = 66912849;`,
56 | `json[""] = 2;`,
57 | })
58 |
59 | t.Logf("Have: %#v", ss)
60 | for _, want := range wants {
61 | if !ss.Contains(want) {
62 | t.Errorf("Statement group should contain `%s` but doesn't", want)
63 | }
64 | }
65 |
66 | }
67 |
68 | func TestStatementsSorting(t *testing.T) {
69 | want := statementsFromStringSlice([]string{
70 | `json.a = true;`,
71 | `json.b = true;`,
72 | `json.c[0] = true;`,
73 | `json.c[2] = true;`,
74 | `json.c[10] = true;`,
75 | `json.c[11] = true;`,
76 | `json.c[21][2] = true;`,
77 | `json.c[21][11] = true;`,
78 | })
79 |
80 | have := statementsFromStringSlice([]string{
81 | `json.c[11] = true;`,
82 | `json.c[21][2] = true;`,
83 | `json.c[0] = true;`,
84 | `json.c[2] = true;`,
85 | `json.b = true;`,
86 | `json.c[10] = true;`,
87 | `json.c[21][11] = true;`,
88 | `json.a = true;`,
89 | })
90 |
91 | sort.Sort(have)
92 |
93 | for i := range want {
94 | if !reflect.DeepEqual(have[i], want[i]) {
95 | t.Errorf("Statements sorted incorrectly; want `%s` at index %d, have `%s`", want[i], i, have[i])
96 | }
97 | }
98 | }
99 |
100 | func BenchmarkStatementsLess(b *testing.B) {
101 | ss := statementsFromStringSlice([]string{
102 | `json.c[21][2] = true;`,
103 | `json.c[21][11] = true;`,
104 | })
105 |
106 | for i := 0; i < b.N; i++ {
107 | _ = ss.Less(0, 1)
108 | }
109 | }
110 |
111 | func BenchmarkFill(b *testing.B) {
112 | j := []byte(`{
113 | "dotted": "A dotted value",
114 | "a quoted": "value",
115 | "bool1": true,
116 | "bool2": false,
117 | "anull": null,
118 | "anarr": [1, 1.5],
119 | "anob": {
120 | "foo": "bar"
121 | },
122 | "else": 1
123 | }`)
124 |
125 | var top interface{}
126 | err := json.Unmarshal(j, &top)
127 | if err != nil {
128 | b.Fatalf("Failed to unmarshal test file: %s", err)
129 | }
130 |
131 | for i := 0; i < b.N; i++ {
132 | ss := make(statements, 0)
133 | ss.fill(statement{{"json", typBare}}, top)
134 | }
135 | }
136 |
137 | func TestUngronStatementsSimple(t *testing.T) {
138 | in := statementsFromStringSlice([]string{
139 | `json.contact = {};`,
140 | `json.contact["e-mail"][0] = "mail@tomnomnom.com";`,
141 | `json.contact["e-mail"][1] = "test@tomnomnom.com";`,
142 | `json.contact["e-mail"][3] = "foo@tomnomnom.com";`,
143 | `json.contact.twitter = "@TomNomNom";`,
144 | })
145 |
146 | want := map[string]interface{}{
147 | "json": map[string]interface{}{
148 | "contact": map[string]interface{}{
149 | "e-mail": []interface{}{
150 | 0: "mail@tomnomnom.com",
151 | 1: "test@tomnomnom.com",
152 | 3: "foo@tomnomnom.com",
153 | },
154 | "twitter": "@TomNomNom",
155 | },
156 | },
157 | }
158 |
159 | have, err := in.toInterface()
160 |
161 | if err != nil {
162 | t.Fatalf("want nil error but have: %s", err)
163 | }
164 |
165 | t.Logf("Have: %#v", have)
166 | t.Logf("Want: %#v", want)
167 |
168 | eq := reflect.DeepEqual(have, want)
169 | if !eq {
170 | t.Errorf("have and want are not equal")
171 | }
172 | }
173 |
174 | func TestUngronStatementsInvalid(t *testing.T) {
175 | cases := []statements{
176 | statementsFromStringSlice([]string{``}),
177 | statementsFromStringSlice([]string{`this isn't a statement at all`}),
178 | statementsFromStringSlice([]string{`json[0] = 1;`, `json.bar = 1;`}),
179 | }
180 |
181 | for _, c := range cases {
182 | _, err := c.toInterface()
183 | if err == nil {
184 | t.Errorf("want non-nil error; have nil")
185 | }
186 | }
187 | }
188 |
189 | func TestStatement(t *testing.T) {
190 | s := statement{
191 | token{"json", typBare},
192 | token{".", typDot},
193 | token{"foo", typBare},
194 | token{"=", typEquals},
195 | token{"2", typNumber},
196 | token{";", typSemi},
197 | }
198 |
199 | have := s.String()
200 | want := "json.foo = 2;"
201 | if have != want {
202 | t.Errorf("have: `%s` want: `%s`", have, want)
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/testdata/github.gron:
--------------------------------------------------------------------------------
1 | json = [];
2 | json[0] = {};
3 | json[0].author = {};
4 | json[0].author.avatar_url = "https://avatars.githubusercontent.com/u/58276?v=3";
5 | json[0].author.events_url = "https://api.github.com/users/tomnomnom/events{/privacy}";
6 | json[0].author.followers_url = "https://api.github.com/users/tomnomnom/followers";
7 | json[0].author.following_url = "https://api.github.com/users/tomnomnom/following{/other_user}";
8 | json[0].author.gists_url = "https://api.github.com/users/tomnomnom/gists{/gist_id}";
9 | json[0].author.gravatar_id = "";
10 | json[0].author.html_url = "https://github.com/tomnomnom";
11 | json[0].author.id = 58276;
12 | json[0].author.login = "tomnomnom";
13 | json[0].author.organizations_url = "https://api.github.com/users/tomnomnom/orgs";
14 | json[0].author.received_events_url = "https://api.github.com/users/tomnomnom/received_events";
15 | json[0].author.repos_url = "https://api.github.com/users/tomnomnom/repos";
16 | json[0].author.site_admin = false;
17 | json[0].author.starred_url = "https://api.github.com/users/tomnomnom/starred{/owner}{/repo}";
18 | json[0].author.subscriptions_url = "https://api.github.com/users/tomnomnom/subscriptions";
19 | json[0].author.type = "User";
20 | json[0].author.url = "https://api.github.com/users/tomnomnom";
21 | json[0].comments_url = "https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e/comments";
22 | json[0].commit = {};
23 | json[0].commit.author = {};
24 | json[0].commit.author.date = "2016-09-09T20:08:58Z";
25 | json[0].commit.author.email = "mail@tomnomnom.com";
26 | json[0].commit.author.name = "Tom Hudson";
27 | json[0].commit.comment_count = 0;
28 | json[0].commit.committer = {};
29 | json[0].commit.committer.date = "2016-09-09T20:08:58Z";
30 | json[0].commit.committer.email = "mail@tomnomnom.com";
31 | json[0].commit.committer.name = "Tom Hudson";
32 | json[0].commit.message = "Unexports statements add/addFull methods";
33 | json[0].commit.tree = {};
34 | json[0].commit.tree.sha = "536ec3c40a0510cd048921cd8757c401aad94b91";
35 | json[0].commit.tree.url = "https://api.github.com/repos/tomnomnom/gron/git/trees/536ec3c40a0510cd048921cd8757c401aad94b91";
36 | json[0].commit.url = "https://api.github.com/repos/tomnomnom/gron/git/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e";
37 | json[0].committer = {};
38 | json[0].committer.avatar_url = "https://avatars.githubusercontent.com/u/58276?v=3";
39 | json[0].committer.events_url = "https://api.github.com/users/tomnomnom/events{/privacy}";
40 | json[0].committer.followers_url = "https://api.github.com/users/tomnomnom/followers";
41 | json[0].committer.following_url = "https://api.github.com/users/tomnomnom/following{/other_user}";
42 | json[0].committer.gists_url = "https://api.github.com/users/tomnomnom/gists{/gist_id}";
43 | json[0].committer.gravatar_id = "";
44 | json[0].committer.html_url = "https://github.com/tomnomnom";
45 | json[0].committer.id = 58276;
46 | json[0].committer.login = "tomnomnom";
47 | json[0].committer.organizations_url = "https://api.github.com/users/tomnomnom/orgs";
48 | json[0].committer.received_events_url = "https://api.github.com/users/tomnomnom/received_events";
49 | json[0].committer.repos_url = "https://api.github.com/users/tomnomnom/repos";
50 | json[0].committer.site_admin = false;
51 | json[0].committer.starred_url = "https://api.github.com/users/tomnomnom/starred{/owner}{/repo}";
52 | json[0].committer.subscriptions_url = "https://api.github.com/users/tomnomnom/subscriptions";
53 | json[0].committer.type = "User";
54 | json[0].committer.url = "https://api.github.com/users/tomnomnom";
55 | json[0].html_url = "https://github.com/tomnomnom/gron/commit/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e";
56 | json[0].parents = [];
57 | json[0].parents[0] = {};
58 | json[0].parents[0].html_url = "https://github.com/tomnomnom/gron/commit/ca66db90d96332a85ec29ea527b3966decf1c6fc";
59 | json[0].parents[0].sha = "ca66db90d96332a85ec29ea527b3966decf1c6fc";
60 | json[0].parents[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/ca66db90d96332a85ec29ea527b3966decf1c6fc";
61 | json[0].sha = "cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e";
62 | json[0].url = "https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e";
63 |
--------------------------------------------------------------------------------
/testdata/github.jgron:
--------------------------------------------------------------------------------
1 | [[],[]]
2 | [[0],{}]
3 | [[0,"author"],{}]
4 | [[0,"author","avatar_url"],"https://avatars.githubusercontent.com/u/58276?v=3"]
5 | [[0,"author","events_url"],"https://api.github.com/users/tomnomnom/events{/privacy}"]
6 | [[0,"author","followers_url"],"https://api.github.com/users/tomnomnom/followers"]
7 | [[0,"author","following_url"],"https://api.github.com/users/tomnomnom/following{/other_user}"]
8 | [[0,"author","gists_url"],"https://api.github.com/users/tomnomnom/gists{/gist_id}"]
9 | [[0,"author","gravatar_id"],""]
10 | [[0,"author","html_url"],"https://github.com/tomnomnom"]
11 | [[0,"author","id"],58276]
12 | [[0,"author","login"],"tomnomnom"]
13 | [[0,"author","organizations_url"],"https://api.github.com/users/tomnomnom/orgs"]
14 | [[0,"author","received_events_url"],"https://api.github.com/users/tomnomnom/received_events"]
15 | [[0,"author","repos_url"],"https://api.github.com/users/tomnomnom/repos"]
16 | [[0,"author","site_admin"],false]
17 | [[0,"author","starred_url"],"https://api.github.com/users/tomnomnom/starred{/owner}{/repo}"]
18 | [[0,"author","subscriptions_url"],"https://api.github.com/users/tomnomnom/subscriptions"]
19 | [[0,"author","type"],"User"]
20 | [[0,"author","url"],"https://api.github.com/users/tomnomnom"]
21 | [[0,"comments_url"],"https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e/comments"]
22 | [[0,"commit"],{}]
23 | [[0,"commit","author"],{}]
24 | [[0,"commit","author","date"],"2016-09-09T20:08:58Z"]
25 | [[0,"commit","author","email"],"mail@tomnomnom.com"]
26 | [[0,"commit","author","name"],"Tom Hudson"]
27 | [[0,"commit","comment_count"],0]
28 | [[0,"commit","committer"],{}]
29 | [[0,"commit","committer","date"],"2016-09-09T20:08:58Z"]
30 | [[0,"commit","committer","email"],"mail@tomnomnom.com"]
31 | [[0,"commit","committer","name"],"Tom Hudson"]
32 | [[0,"commit","message"],"Unexports statements add/addFull methods"]
33 | [[0,"commit","tree"],{}]
34 | [[0,"commit","tree","sha"],"536ec3c40a0510cd048921cd8757c401aad94b91"]
35 | [[0,"commit","tree","url"],"https://api.github.com/repos/tomnomnom/gron/git/trees/536ec3c40a0510cd048921cd8757c401aad94b91"]
36 | [[0,"commit","url"],"https://api.github.com/repos/tomnomnom/gron/git/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"]
37 | [[0,"committer"],{}]
38 | [[0,"committer","avatar_url"],"https://avatars.githubusercontent.com/u/58276?v=3"]
39 | [[0,"committer","events_url"],"https://api.github.com/users/tomnomnom/events{/privacy}"]
40 | [[0,"committer","followers_url"],"https://api.github.com/users/tomnomnom/followers"]
41 | [[0,"committer","following_url"],"https://api.github.com/users/tomnomnom/following{/other_user}"]
42 | [[0,"committer","gists_url"],"https://api.github.com/users/tomnomnom/gists{/gist_id}"]
43 | [[0,"committer","gravatar_id"],""]
44 | [[0,"committer","html_url"],"https://github.com/tomnomnom"]
45 | [[0,"committer","id"],58276]
46 | [[0,"committer","login"],"tomnomnom"]
47 | [[0,"committer","organizations_url"],"https://api.github.com/users/tomnomnom/orgs"]
48 | [[0,"committer","received_events_url"],"https://api.github.com/users/tomnomnom/received_events"]
49 | [[0,"committer","repos_url"],"https://api.github.com/users/tomnomnom/repos"]
50 | [[0,"committer","site_admin"],false]
51 | [[0,"committer","starred_url"],"https://api.github.com/users/tomnomnom/starred{/owner}{/repo}"]
52 | [[0,"committer","subscriptions_url"],"https://api.github.com/users/tomnomnom/subscriptions"]
53 | [[0,"committer","type"],"User"]
54 | [[0,"committer","url"],"https://api.github.com/users/tomnomnom"]
55 | [[0,"html_url"],"https://github.com/tomnomnom/gron/commit/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"]
56 | [[0,"parents"],[]]
57 | [[0,"parents",0],{}]
58 | [[0,"parents",0,"html_url"],"https://github.com/tomnomnom/gron/commit/ca66db90d96332a85ec29ea527b3966decf1c6fc"]
59 | [[0,"parents",0,"sha"],"ca66db90d96332a85ec29ea527b3966decf1c6fc"]
60 | [[0,"parents",0,"url"],"https://api.github.com/repos/tomnomnom/gron/commits/ca66db90d96332a85ec29ea527b3966decf1c6fc"]
61 | [[0,"sha"],"cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"]
62 | [[0,"url"],"https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e"]
63 |
--------------------------------------------------------------------------------
/testdata/github.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "sha": "cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e",
4 | "commit": {
5 | "author": {
6 | "name": "Tom Hudson",
7 | "email": "mail@tomnomnom.com",
8 | "date": "2016-09-09T20:08:58Z"
9 | },
10 | "committer": {
11 | "name": "Tom Hudson",
12 | "email": "mail@tomnomnom.com",
13 | "date": "2016-09-09T20:08:58Z"
14 | },
15 | "message": "Unexports statements add/addFull methods",
16 | "tree": {
17 | "sha": "536ec3c40a0510cd048921cd8757c401aad94b91",
18 | "url": "https://api.github.com/repos/tomnomnom/gron/git/trees/536ec3c40a0510cd048921cd8757c401aad94b91"
19 | },
20 | "url": "https://api.github.com/repos/tomnomnom/gron/git/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e",
21 | "comment_count": 0
22 | },
23 | "url": "https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e",
24 | "html_url": "https://github.com/tomnomnom/gron/commit/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e",
25 | "comments_url": "https://api.github.com/repos/tomnomnom/gron/commits/cfade78b10e9c0cb9e4e0a902f5383ee4e5efc9e/comments",
26 | "author": {
27 | "login": "tomnomnom",
28 | "id": 58276,
29 | "avatar_url": "https://avatars.githubusercontent.com/u/58276?v=3",
30 | "gravatar_id": "",
31 | "url": "https://api.github.com/users/tomnomnom",
32 | "html_url": "https://github.com/tomnomnom",
33 | "followers_url": "https://api.github.com/users/tomnomnom/followers",
34 | "following_url": "https://api.github.com/users/tomnomnom/following{/other_user}",
35 | "gists_url": "https://api.github.com/users/tomnomnom/gists{/gist_id}",
36 | "starred_url": "https://api.github.com/users/tomnomnom/starred{/owner}{/repo}",
37 | "subscriptions_url": "https://api.github.com/users/tomnomnom/subscriptions",
38 | "organizations_url": "https://api.github.com/users/tomnomnom/orgs",
39 | "repos_url": "https://api.github.com/users/tomnomnom/repos",
40 | "events_url": "https://api.github.com/users/tomnomnom/events{/privacy}",
41 | "received_events_url": "https://api.github.com/users/tomnomnom/received_events",
42 | "type": "User",
43 | "site_admin": false
44 | },
45 | "committer": {
46 | "login": "tomnomnom",
47 | "id": 58276,
48 | "avatar_url": "https://avatars.githubusercontent.com/u/58276?v=3",
49 | "gravatar_id": "",
50 | "url": "https://api.github.com/users/tomnomnom",
51 | "html_url": "https://github.com/tomnomnom",
52 | "followers_url": "https://api.github.com/users/tomnomnom/followers",
53 | "following_url": "https://api.github.com/users/tomnomnom/following{/other_user}",
54 | "gists_url": "https://api.github.com/users/tomnomnom/gists{/gist_id}",
55 | "starred_url": "https://api.github.com/users/tomnomnom/starred{/owner}{/repo}",
56 | "subscriptions_url": "https://api.github.com/users/tomnomnom/subscriptions",
57 | "organizations_url": "https://api.github.com/users/tomnomnom/orgs",
58 | "repos_url": "https://api.github.com/users/tomnomnom/repos",
59 | "events_url": "https://api.github.com/users/tomnomnom/events{/privacy}",
60 | "received_events_url": "https://api.github.com/users/tomnomnom/received_events",
61 | "type": "User",
62 | "site_admin": false
63 | },
64 | "parents": [
65 | {
66 | "sha": "ca66db90d96332a85ec29ea527b3966decf1c6fc",
67 | "url": "https://api.github.com/repos/tomnomnom/gron/commits/ca66db90d96332a85ec29ea527b3966decf1c6fc",
68 | "html_url": "https://github.com/tomnomnom/gron/commit/ca66db90d96332a85ec29ea527b3966decf1c6fc"
69 | }
70 | ]
71 | }
72 | ]
73 |
--------------------------------------------------------------------------------
/testdata/grep-separators.gron:
--------------------------------------------------------------------------------
1 | json.contact.email = "mail@tomnomnom.com";
2 | json.contact.twitter = "@TomNomNom";
3 | --
4 | json.likes[2] = "meat";
5 | json.name = "Tom";
6 |
--------------------------------------------------------------------------------
/testdata/grep-separators.json:
--------------------------------------------------------------------------------
1 | {
2 | "contact": {
3 | "email": "mail@tomnomnom.com",
4 | "twitter": "@TomNomNom"
5 | },
6 | "likes": [
7 | null,
8 | null,
9 | "meat"
10 | ],
11 | "name": "Tom"
12 | }
13 |
--------------------------------------------------------------------------------
/testdata/invalid-type-mismatch.gron:
--------------------------------------------------------------------------------
1 | json.foo = {};
2 | json.foo = [];
3 |
--------------------------------------------------------------------------------
/testdata/invalid-value.gron:
--------------------------------------------------------------------------------
1 | json = {};
2 | json.contact = {};
3 | json.contact.email = "mail@tomnomnom.com";
4 | json.contact.twitter = "@TomNomNom";
5 | json.github = "https://github.com/tomnomnom/";
6 | json.likes = [;
7 | json.likes[0] = "code";
8 | json.likes[1] = "cheese";
9 | json.likes[2] = "meat";
10 | json.name = "Tom";
11 |
--------------------------------------------------------------------------------
/testdata/long-stream.json:
--------------------------------------------------------------------------------
1 | {"details":[{"_id":"5ae0aa34b3f979b9b85509e0","index":0,"guid":"164542db-58d9-4d63-aacd-fbf3a7739474","isActive":true,"balance":"$2,644.04","picture":"http://placehold.it/32x32","age":38,"eyeColor":"blue","name":{"first":"Tracy","last":"Scott"},"company":"QUOTEZART","email":"tracy.scott@quotezart.io","phone":"+1 (998) 407-3468","address":"324 Cornelia Street, Echo, Indiana, 8046","about":"Nostrud magna ullamco id amet et incididunt officia dolor cillum do. Excepteur amet aliquip non veniam amet est nisi incididunt enim aliqua ad occaecat exercitation. Duis cupidatat ea voluptate consequat anim irure incididunt consequat consectetur amet. Enim exercitation ea eiusmod aliqua occaecat proident proident occaecat incididunt fugiat sint. Eu velit incididunt dolore voluptate in deserunt mollit officia.","registered":"Saturday, October 7, 2017 2:37 PM","latitude":"-19.206275","longitude":"-57.387994","tags":["quis","elit","minim","quis","ut","quis","et","laboris","et","quis"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Gwendolyn Barnett"},{"id":1,"name":"Hill Patrick"},{"id":2,"name":"Corinne Pearson"},{"id":3,"name":"Nona Walters"},{"id":4,"name":"Beach Hart"},{"id":5,"name":"Figueroa Russell"},{"id":6,"name":"Eve Cobb"},{"id":7,"name":"Sanchez Bradshaw"},{"id":8,"name":"Irma Turner"},{"id":9,"name":"Keisha Walls"}],"greeting":"Hello, Tracy! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa342f534020c0f882a2","index":1,"guid":"e3ceb40a-9dbd-4769-96cd-283c227952d4","isActive":true,"balance":"$2,449.54","picture":"http://placehold.it/32x32","age":33,"eyeColor":"green","name":{"first":"Nora","last":"Gates"},"company":"ENTALITY","email":"nora.gates@entality.name","phone":"+1 (845) 592-3644","address":"904 Randolph Street, Cowiche, District Of Columbia, 6897","about":"Duis in mollit ex consectetur laboris commodo ad laboris do officia. Proident aute officia aliquip mollit incididunt amet est deserunt. Sunt magna ea sit reprehenderit. Enim et non excepteur dolor proident quis dolor ea. Non dolore tempor esse aliqua ipsum cupidatat quis cupidatat sint veniam pariatur.","registered":"Wednesday, September 3, 2014 5:55 PM","latitude":"-17.863169","longitude":"175.110579","tags":["sunt","ut","sint","labore","adipisicing","velit","esse","consectetur","enim","ad"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Meagan Daugherty"},{"id":1,"name":"Morin Delacruz"},{"id":2,"name":"Robin Blanchard"},{"id":3,"name":"Georgia Mays"},{"id":4,"name":"Cheri Soto"},{"id":5,"name":"Hobbs Lucas"},{"id":6,"name":"Maritza Garza"},{"id":7,"name":"Kathy Whitney"},{"id":8,"name":"Burch Cortez"},{"id":9,"name":"Jenny Campos"}],"greeting":"Hello, Nora! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34866f48fd0a12505e","index":2,"guid":"ddcd3d1e-8706-48bf-9dea-554b69706120","isActive":false,"balance":"$3,999.63","picture":"http://placehold.it/32x32","age":20,"eyeColor":"green","name":{"first":"Beatrice","last":"Foley"},"company":"KANGLE","email":"beatrice.foley@kangle.biz","phone":"+1 (832) 487-2607","address":"813 Garden Place, Hackneyville, Kentucky, 4301","about":"Minim dolor eiusmod ipsum laborum proident. Ad cillum ullamco cupidatat dolor proident irure veniam proident nostrud. Enim non cillum nostrud tempor dolor id dolore laboris mollit non adipisicing. In dolore amet dolor incididunt non dolor magna pariatur anim exercitation eiusmod reprehenderit. Ex elit aute eiusmod ullamco amet consequat nostrud est. Ullamco cupidatat adipisicing amet commodo non ipsum aliqua esse excepteur reprehenderit irure quis consectetur enim. Eu aliqua occaecat do amet exercitation ad fugiat.","registered":"Thursday, August 27, 2015 6:31 PM","latitude":"-20.102055","longitude":"-159.81829","tags":["reprehenderit","et","magna","aliquip","sit","pariatur","amet","labore","Lorem","aute"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Lawrence Ratliff"},{"id":1,"name":"Debbie Kent"},{"id":2,"name":"Lynda Morse"},{"id":3,"name":"Wiley Stephenson"},{"id":4,"name":"Mclaughlin Hines"},{"id":5,"name":"Erin Ashley"},{"id":6,"name":"Boyer Dotson"},{"id":7,"name":"Decker Rollins"},{"id":8,"name":"Eddie Lane"},{"id":9,"name":"Cecile Lindsey"}],"greeting":"Hello, Beatrice! You have 7 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34dc3fbdc4ed72a342","index":3,"guid":"5dbc0677-c792-420e-b31e-2ba3e520b9bf","isActive":false,"balance":"$1,227.64","picture":"http://placehold.it/32x32","age":20,"eyeColor":"brown","name":{"first":"Sherrie","last":"Brock"},"company":"NAVIR","email":"sherrie.brock@navir.us","phone":"+1 (886) 472-3610","address":"916 Oakland Place, Rodman, Louisiana, 8864","about":"Eiusmod est minim ullamco minim tempor sunt. Do aliquip tempor et nisi aute do ullamco. Sint non mollit fugiat Lorem laboris pariatur et commodo labore et tempor. Non officia tempor cupidatat aute deserunt ea ut laboris consequat magna.","registered":"Saturday, November 12, 2016 3:31 AM","latitude":"54.818965","longitude":"117.261723","tags":["veniam","voluptate","esse","amet","deserunt","aute","elit","anim","est","excepteur"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Sofia Ray"},{"id":1,"name":"Ronda Bates"},{"id":2,"name":"Robert Blankenship"},{"id":3,"name":"Bowers Potts"},{"id":4,"name":"Holcomb Nieves"},{"id":5,"name":"Marquita Glover"},{"id":6,"name":"Manuela Rocha"},{"id":7,"name":"Carey Fitzpatrick"},{"id":8,"name":"Joann Singleton"},{"id":9,"name":"Vincent Johns"}],"greeting":"Hello, Sherrie! You have 8 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa348f55f735b48cfa46","index":4,"guid":"a1944b22-c22f-49a3-8ac4-45153a3d1de4","isActive":true,"balance":"$2,403.86","picture":"http://placehold.it/32x32","age":22,"eyeColor":"brown","name":{"first":"Stuart","last":"Mendoza"},"company":"SNOWPOKE","email":"stuart.mendoza@snowpoke.biz","phone":"+1 (940) 532-2198","address":"806 High Street, Dunnavant, Idaho, 5507","about":"Aute adipisicing deserunt et elit consequat deserunt amet amet officia aliqua in. Et qui minim ea sit exercitation aliquip do laborum consectetur voluptate eiusmod irure. Exercitation reprehenderit eiusmod proident fugiat laborum in aliqua laboris commodo sint et. Amet qui labore culpa excepteur magna voluptate anim mollit eiusmod. Officia mollit eu et laborum reprehenderit ullamco. Proident incididunt incididunt cillum laborum officia veniam aute voluptate officia irure elit.","registered":"Wednesday, November 12, 2014 9:10 AM","latitude":"-4.553307","longitude":"-92.981375","tags":["voluptate","fugiat","non","fugiat","cupidatat","deserunt","occaecat","dolor","aute","fugiat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Valeria Burgess"},{"id":1,"name":"Cleveland Noel"},{"id":2,"name":"Arline Oneill"},{"id":3,"name":"Tasha Johnston"},{"id":4,"name":"Graciela Mosley"},{"id":5,"name":"Bartlett Gonzales"},{"id":6,"name":"Darla Bond"},{"id":7,"name":"Gail Hatfield"},{"id":8,"name":"Cecilia Clay"},{"id":9,"name":"Keri May"}],"greeting":"Hello, Stuart! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa342f229045f41285f7","index":5,"guid":"a087b56e-cd7c-4832-9c07-ec80217d5355","isActive":false,"balance":"$3,736.67","picture":"http://placehold.it/32x32","age":27,"eyeColor":"blue","name":{"first":"Marjorie","last":"Cunningham"},"company":"IMMUNICS","email":"marjorie.cunningham@immunics.com","phone":"+1 (807) 427-2665","address":"649 Whitty Lane, Thatcher, Michigan, 9747","about":"Aliquip quis proident consequat magna id ullamco nulla culpa laborum. Quis laborum laborum adipisicing duis duis eu sint exercitation deserunt cillum ea. Incididunt laborum magna Lorem quis officia sint cillum commodo laboris exercitation proident proident. Sit sunt nostrud sint duis. Cillum minim cupidatat sunt ipsum eiusmod ipsum fugiat fugiat culpa consequat. Esse irure consectetur ad officia Lorem eu culpa est ad deserunt velit amet exercitation aute.","registered":"Tuesday, October 7, 2014 5:24 PM","latitude":"6.552001","longitude":"-139.202433","tags":["ipsum","est","officia","eiusmod","ea","fugiat","quis","voluptate","proident","dolor"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Glenna Beck"},{"id":1,"name":"Castaneda Spears"},{"id":2,"name":"Justice Michael"},{"id":3,"name":"Shelton Parsons"},{"id":4,"name":"Robertson Good"},{"id":5,"name":"Sheena Burch"},{"id":6,"name":"Tessa Miranda"},{"id":7,"name":"Perkins Barker"},{"id":8,"name":"Maribel Lester"},{"id":9,"name":"Hodge Cabrera"}],"greeting":"Hello, Marjorie! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa3482a305f0781a3aba","index":6,"guid":"ccfdf87d-b381-4b64-b517-cd60a8fc9f04","isActive":false,"balance":"$2,116.04","picture":"http://placehold.it/32x32","age":21,"eyeColor":"green","name":{"first":"Yvonne","last":"Duffy"},"company":"KENEGY","email":"yvonne.duffy@kenegy.co.uk","phone":"+1 (956) 575-2780","address":"742 Bergen Court, Mahtowa, Alaska, 4598","about":"Anim nulla est eu laborum proident et. Sunt anim dolore voluptate non laboris sint mollit laborum exercitation enim aliqua exercitation. Veniam consectetur excepteur esse nostrud cupidatat aute nulla aute ad id mollit. Enim exercitation labore culpa voluptate in ea nulla nulla ex. Id tempor nisi ad proident dolore est cillum eiusmod dolor veniam consectetur labore. Mollit velit cupidatat est mollit adipisicing est exercitation. Veniam aliqua reprehenderit ea tempor.","registered":"Monday, December 8, 2014 2:57 AM","latitude":"23.863564","longitude":"150.896594","tags":["voluptate","ex","consequat","in","sit","nulla","commodo","adipisicing","nulla","commodo"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Clemons Snyder"},{"id":1,"name":"Bernard Navarro"},{"id":2,"name":"Burton Burnett"},{"id":3,"name":"Suzette Callahan"},{"id":4,"name":"William Hill"},{"id":5,"name":"Mays Larsen"},{"id":6,"name":"Joanna Graham"},{"id":7,"name":"Fowler Berry"},{"id":8,"name":"Lacey Hayden"},{"id":9,"name":"Shauna Frederick"}],"greeting":"Hello, Yvonne! You have 6 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa349dcd18765c1626dc","index":7,"guid":"425bc7f1-467f-4ed6-be2c-ccbcd9555674","isActive":false,"balance":"$3,403.00","picture":"http://placehold.it/32x32","age":33,"eyeColor":"blue","name":{"first":"Dana","last":"Reed"},"company":"NETPLAX","email":"dana.reed@netplax.ca","phone":"+1 (988) 463-2342","address":"980 Central Avenue, Strykersville, Maryland, 873","about":"Sunt nisi duis dolor cupidatat magna quis. Pariatur eu id commodo ea eiusmod dolore cillum veniam voluptate. Anim ex occaecat sit ipsum proident tempor quis ipsum laborum ad. Minim ut quis enim incididunt aliqua tempor. Ullamco exercitation consectetur quis fugiat cupidatat esse est consequat ad aliqua duis ea consectetur consequat. Id ea commodo minim aute.","registered":"Sunday, March 22, 2015 7:03 PM","latitude":"61.534583","longitude":"123.100188","tags":["non","elit","Lorem","commodo","enim","dolor","sint","qui","aliqua","cupidatat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Chan Keller"},{"id":1,"name":"Mary Dyer"},{"id":2,"name":"Genevieve Whitfield"},{"id":3,"name":"Meyers York"},{"id":4,"name":"James Carr"},{"id":5,"name":"Rollins Frazier"},{"id":6,"name":"Aurora Nguyen"},{"id":7,"name":"Shawn Jacobs"},{"id":8,"name":"Reba Mcfarland"},{"id":9,"name":"Paige Pitts"}],"greeting":"Hello, Dana! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34d760d6da02fde756","index":8,"guid":"be208b32-be08-4c6d-86ea-da589d1ec977","isActive":false,"balance":"$1,012.38","picture":"http://placehold.it/32x32","age":23,"eyeColor":"brown","name":{"first":"Schmidt","last":"Sweeney"},"company":"PROWASTE","email":"schmidt.sweeney@prowaste.info","phone":"+1 (994) 493-3642","address":"245 Veterans Avenue, Catherine, Arkansas, 1444","about":"Voluptate ut deserunt nostrud nulla anim fugiat occaecat fugiat nisi anim commodo minim consequat. Tempor velit amet cillum aliquip nisi ut. Eu sunt excepteur amet dolor excepteur laborum. Enim minim elit irure proident pariatur ex nulla aute sunt.","registered":"Wednesday, April 6, 2016 12:10 PM","latitude":"22.555683","longitude":"63.232647","tags":["aute","deserunt","ad","sint","quis","tempor","sunt","consectetur","labore","Lorem"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Browning House"},{"id":1,"name":"Beverly Hoover"},{"id":2,"name":"Mitchell Figueroa"},{"id":3,"name":"Blackburn Camacho"},{"id":4,"name":"Leila Doyle"},{"id":5,"name":"Lee Montoya"},{"id":6,"name":"Beck Velasquez"},{"id":7,"name":"Charlotte Bird"},{"id":8,"name":"Lillie Velez"},{"id":9,"name":"Williamson Elliott"}],"greeting":"Hello, Schmidt! You have 6 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34a2574825a9fad671","index":9,"guid":"b3a104cf-4361-421c-9e13-91ae146e517d","isActive":true,"balance":"$1,454.62","picture":"http://placehold.it/32x32","age":38,"eyeColor":"blue","name":{"first":"Davenport","last":"Marquez"},"company":"MUSANPOLY","email":"davenport.marquez@musanpoly.me","phone":"+1 (843) 438-2469","address":"109 Victor Road, Fruitdale, American Samoa, 8856","about":"Culpa aliquip Lorem elit laborum aliquip laboris dolore non ea. Est occaecat do minim pariatur pariatur mollit velit laborum eu consectetur qui eiusmod. Consectetur nostrud occaecat dolore sunt mollit. Anim excepteur et nulla consequat laborum dolor Lorem ad.","registered":"Sunday, February 16, 2014 11:28 PM","latitude":"5.32883","longitude":"-128.790048","tags":["veniam","cillum","non","qui","ullamco","quis","consequat","incididunt","laborum","deserunt"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Beard Gomez"},{"id":1,"name":"Elaine Lamb"},{"id":2,"name":"Gallegos Merritt"},{"id":3,"name":"Margarita Shepherd"},{"id":4,"name":"Mia Griffin"},{"id":5,"name":"Deena Foreman"},{"id":6,"name":"Muriel Howe"},{"id":7,"name":"Wilder Chang"},{"id":8,"name":"Pearlie Ford"},{"id":9,"name":"Tabatha Mathews"}],"greeting":"Hello, Davenport! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34ce8ffcd27683f469","index":10,"guid":"d6d9c317-c71c-48ff-a522-905d54cef18b","isActive":true,"balance":"$2,197.16","picture":"http://placehold.it/32x32","age":39,"eyeColor":"green","name":{"first":"Julianne","last":"Miles"},"company":"EXIAND","email":"julianne.miles@exiand.org","phone":"+1 (981) 429-3214","address":"560 Louis Place, Sperryville, Vermont, 7450","about":"Aute irure Lorem laborum cillum Lorem ad dolore proident tempor in non. Excepteur fugiat amet proident ut exercitation adipisicing cupidatat esse. Occaecat cillum labore dolore cupidatat consectetur est exercitation mollit dolor dolore.","registered":"Wednesday, June 14, 2017 11:19 AM","latitude":"-32.254528","longitude":"-97.815247","tags":["consectetur","cupidatat","est","esse","anim","et","tempor","et","ex","fugiat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Trevino Nash"},{"id":1,"name":"Mills Conley"},{"id":2,"name":"Deirdre Cruz"},{"id":3,"name":"Marsh Webster"},{"id":4,"name":"Frye Johnson"},{"id":5,"name":"Jeanine Craft"},{"id":6,"name":"Fern Richmond"},{"id":7,"name":"Janelle Ramsey"},{"id":8,"name":"Kirby Valentine"},{"id":9,"name":"Lessie Acevedo"}],"greeting":"Hello, Julianne! You have 8 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34d5b29d6058c2450f","index":11,"guid":"925f4c31-5a6b-4cf2-b90d-fb0ee37680a6","isActive":false,"balance":"$1,111.42","picture":"http://placehold.it/32x32","age":34,"eyeColor":"blue","name":{"first":"Elva","last":"Woods"},"company":"MAXIMIND","email":"elva.woods@maximind.tv","phone":"+1 (883) 569-3812","address":"540 Trucklemans Lane, Stollings, Oregon, 2549","about":"Consequat ut deserunt in esse do nisi. Aliquip laboris ex nulla excepteur occaecat et nulla. Incididunt enim irure officia ex aute nulla est consectetur aute eiusmod. Ea ipsum magna nisi quis ex labore qui occaecat eu aute culpa adipisicing aute cupidatat. Consequat mollit dolore ex deserunt. Deserunt do eiusmod Lorem anim laborum dolor esse aute laboris dolor officia esse officia. Cupidatat consectetur veniam ipsum et mollit cupidatat ullamco et non officia exercitation nisi ea.","registered":"Saturday, July 25, 2015 1:20 PM","latitude":"-14.967803","longitude":"160.339242","tags":["incididunt","nulla","aliquip","et","quis","voluptate","aliqua","qui","in","ea"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Compton Holt"},{"id":1,"name":"Christy Padilla"},{"id":2,"name":"Roy Boyer"},{"id":3,"name":"Celina Wright"},{"id":4,"name":"Patton Fowler"},{"id":5,"name":"Juanita Blair"},{"id":6,"name":"Thornton Townsend"},{"id":7,"name":"Mcintyre William"},{"id":8,"name":"Lorena Mckee"},{"id":9,"name":"Alisha Mullen"}],"greeting":"Hello, Elva! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34040ef52f7d92ee6a","index":12,"guid":"9ccba22d-2de3-4784-b291-93a18c017a82","isActive":false,"balance":"$1,147.66","picture":"http://placehold.it/32x32","age":26,"eyeColor":"green","name":{"first":"Lizzie","last":"Melton"},"company":"FUTURITY","email":"lizzie.melton@futurity.io","phone":"+1 (875) 595-2680","address":"584 Indiana Place, Blue, New Jersey, 420","about":"Culpa culpa irure duis fugiat magna cillum ut aliqua incididunt. Deserunt nostrud cillum duis reprehenderit ad duis voluptate dolore. Elit officia ullamco minim nisi officia eu eu Lorem sint. Est aute quis occaecat sunt. Excepteur minim commodo exercitation consectetur eiusmod aute elit proident nulla enim ullamco enim. Tempor labore duis ex cupidatat et.","registered":"Friday, October 17, 2014 2:39 PM","latitude":"-52.473183","longitude":"111.529289","tags":["enim","ut","ullamco","tempor","commodo","esse","nisi","eiusmod","exercitation","sit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Sasha Porter"},{"id":1,"name":"Pittman Wilkerson"},{"id":2,"name":"Lloyd Carter"},{"id":3,"name":"Mathews Armstrong"},{"id":4,"name":"Geraldine Harrison"},{"id":5,"name":"Margo Giles"},{"id":6,"name":"Ewing Pollard"},{"id":7,"name":"Esperanza Simon"},{"id":8,"name":"Christa Wiggins"},{"id":9,"name":"Lorraine Wilkinson"}],"greeting":"Hello, Lizzie! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa3468cb0abe9cf31577","index":13,"guid":"3a1566ab-fb93-417b-8c4f-ead8269d6cc8","isActive":false,"balance":"$1,824.72","picture":"http://placehold.it/32x32","age":22,"eyeColor":"blue","name":{"first":"Ford","last":"Randolph"},"company":"INSECTUS","email":"ford.randolph@insectus.name","phone":"+1 (982) 589-2146","address":"231 Centre Street, Coalmont, Washington, 5554","about":"Commodo consequat occaecat ad ea ut id reprehenderit laborum elit ullamco magna. Non aliquip nisi fugiat anim veniam consequat eu qui esse sint magna. Cupidatat officia sunt mollit irure sit nulla excepteur dolor nulla. Fugiat elit deserunt mollit est fugiat laborum magna sint ullamco et in. Commodo voluptate aliqua sit dolore ut excepteur laborum qui dolore.","registered":"Friday, February 28, 2014 9:04 PM","latitude":"-85.796706","longitude":"82.662603","tags":["amet","exercitation","proident","do","incididunt","nisi","sunt","ullamco","commodo","reprehenderit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Delaney Knowles"},{"id":1,"name":"Simone Nichols"},{"id":2,"name":"Tate Brewer"},{"id":3,"name":"Madeline Paul"},{"id":4,"name":"Jeanette Roberson"},{"id":5,"name":"Sallie Holden"},{"id":6,"name":"Clements Hinton"},{"id":7,"name":"Lourdes Tyler"},{"id":8,"name":"Aline Davidson"},{"id":9,"name":"Tia Collier"}],"greeting":"Hello, Ford! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34d4c1ab9766cdd713","index":14,"guid":"0665f792-8282-4efd-b409-9c791ecb1091","isActive":true,"balance":"$2,174.56","picture":"http://placehold.it/32x32","age":35,"eyeColor":"brown","name":{"first":"Grace","last":"Fuentes"},"company":"ZINCA","email":"grace.fuentes@zinca.biz","phone":"+1 (802) 553-2237","address":"166 Sackman Street, Brownsville, Utah, 7296","about":"Lorem incididunt deserunt dolor nulla. Minim sit eu do commodo. Sint aliqua et exercitation duis Lorem laborum irure enim quis pariatur laboris incididunt veniam magna. Ea labore eiusmod incididunt fugiat. Sunt cillum nisi culpa Lorem aliqua nostrud quis dolore incididunt nostrud amet anim cupidatat. Ex pariatur enim esse sunt eiusmod. Aliquip consectetur reprehenderit nisi labore eu nostrud sint consectetur.","registered":"Friday, December 18, 2015 11:18 PM","latitude":"-66.13255","longitude":"179.72211","tags":["ea","sunt","elit","Lorem","nisi","aute","do","fugiat","in","fugiat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Dixon Fletcher"},{"id":1,"name":"Lambert Ingram"},{"id":2,"name":"Mendez Pate"},{"id":3,"name":"Ray Ward"},{"id":4,"name":"Heidi Cardenas"},{"id":5,"name":"Barton Meyer"},{"id":6,"name":"Velma Park"},{"id":7,"name":"Patricia Lynch"},{"id":8,"name":"Eugenia Grant"},{"id":9,"name":"York Sloan"}],"greeting":"Hello, Grace! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa343b56f5acad827190","index":15,"guid":"4c8eb809-8024-490f-b95a-0cace6949320","isActive":true,"balance":"$2,489.41","picture":"http://placehold.it/32x32","age":33,"eyeColor":"green","name":{"first":"Laurel","last":"Contreras"},"company":"DOGNOSIS","email":"laurel.contreras@dognosis.us","phone":"+1 (917) 546-3266","address":"147 Strong Place, Hayes, New Mexico, 2530","about":"Cupidatat nisi quis irure nostrud labore eu et voluptate. Nisi eiusmod enim dolor cillum. Ea dolor et ea adipisicing minim pariatur ea ad minim ad eu amet ea.","registered":"Saturday, December 24, 2016 9:20 AM","latitude":"-54.412123","longitude":"-169.324571","tags":["reprehenderit","quis","irure","dolor","do","adipisicing","veniam","in","aliqua","ipsum"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Laurie Becker"},{"id":1,"name":"Butler Coffey"},{"id":2,"name":"Ruthie Benjamin"},{"id":3,"name":"Jean Lee"},{"id":4,"name":"Lowe Warren"},{"id":5,"name":"Zelma Hudson"},{"id":6,"name":"Wiggins Schwartz"},{"id":7,"name":"Brewer Goodman"},{"id":8,"name":"Melanie Haney"},{"id":9,"name":"Alyson Brooks"}],"greeting":"Hello, Laurel! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa3405bf5c4f96c29069","index":16,"guid":"e218adf6-22b7-4e1e-a6f4-31234460fd8e","isActive":true,"balance":"$1,324.19","picture":"http://placehold.it/32x32","age":31,"eyeColor":"green","name":{"first":"Ginger","last":"Pennington"},"company":"CYTREX","email":"ginger.pennington@cytrex.biz","phone":"+1 (827) 512-3179","address":"726 Lake Street, Shindler, Nevada, 4750","about":"Deserunt elit et labore minim officia cupidatat exercitation esse adipisicing enim amet culpa in adipisicing. Amet eiusmod velit amet laboris deserunt minim. Anim duis deserunt proident ex consequat incididunt commodo sint excepteur Lorem proident esse duis. Ad adipisicing veniam anim Lorem sit eu laboris ex nostrud qui nostrud ipsum. Labore exercitation excepteur voluptate aliqua labore anim id fugiat.","registered":"Saturday, December 9, 2017 10:11 AM","latitude":"14.567831","longitude":"66.731219","tags":["incididunt","dolor","commodo","eiusmod","est","est","sint","ad","ut","minim"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Iva Guerrero"},{"id":1,"name":"Cathleen Young"},{"id":2,"name":"Welch Pierce"},{"id":3,"name":"Tyson Raymond"},{"id":4,"name":"Josefa Crawford"},{"id":5,"name":"Cross Burks"},{"id":6,"name":"Crosby Brady"},{"id":7,"name":"Barry Guthrie"},{"id":8,"name":"Margret Duran"},{"id":9,"name":"Reilly Patton"}],"greeting":"Hello, Ginger! You have 7 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa343537eccc1ace3207","index":17,"guid":"7e65b665-28a9-4499-81c0-c1d9042bebdf","isActive":false,"balance":"$3,776.91","picture":"http://placehold.it/32x32","age":36,"eyeColor":"green","name":{"first":"Mckenzie","last":"Chan"},"company":"ENDIPIN","email":"mckenzie.chan@endipin.com","phone":"+1 (824) 566-2134","address":"448 Terrace Place, Cetronia, Puerto Rico, 9914","about":"Deserunt elit nostrud cupidatat labore deserunt cupidatat ea cupidatat consectetur. Labore est velit duis voluptate occaecat quis veniam nostrud incididunt proident minim elit. Nulla velit elit tempor occaecat cillum adipisicing exercitation exercitation id incididunt officia ipsum fugiat sunt. Aliqua pariatur reprehenderit in aliqua esse do quis aliquip. Proident exercitation ullamco nisi id minim id cillum eiusmod mollit ea Lorem. Laborum sit exercitation exercitation Lorem eiusmod ad laborum nisi ex id non.","registered":"Friday, June 23, 2017 11:32 AM","latitude":"-13.394667","longitude":"-158.627154","tags":["ut","quis","laboris","ea","esse","non","do","irure","ea","excepteur"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Woodward Little"},{"id":1,"name":"Patty Gamble"},{"id":2,"name":"Erickson Macias"},{"id":3,"name":"Tanya Pace"},{"id":4,"name":"Hunt Tate"},{"id":5,"name":"Maricela Crane"},{"id":6,"name":"Hanson Rodriguez"},{"id":7,"name":"Goodman Austin"},{"id":8,"name":"Short Donovan"},{"id":9,"name":"Blanche England"}],"greeting":"Hello, Mckenzie! You have 8 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa34290ac8fa63a12eac","index":18,"guid":"547f7c12-6dc2-46bd-ba15-29730366275d","isActive":true,"balance":"$2,585.89","picture":"http://placehold.it/32x32","age":27,"eyeColor":"green","name":{"first":"Frazier","last":"Pickett"},"company":"GRUPOLI","email":"frazier.pickett@grupoli.co.uk","phone":"+1 (897) 581-2849","address":"678 Newton Street, Chelsea, Ohio, 490","about":"Ex non cupidatat non cupidatat enim sint occaecat ut incididunt irure nulla. Ex id culpa voluptate id do id exercitation sit mollit esse id excepteur eu duis. Minim est veniam laboris irure ut minim consectetur fugiat enim ullamco minim. Sint mollit ullamco dolore consequat ipsum eu cillum. Et consectetur elit est sit aliquip et non occaecat.","registered":"Wednesday, March 29, 2017 7:15 PM","latitude":"1.80887","longitude":"28.819849","tags":["tempor","veniam","elit","mollit","aliquip","magna","laboris","labore","anim","proident"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Mcconnell Mccall"},{"id":1,"name":"Harper Fields"},{"id":2,"name":"Kathryn Schultz"},{"id":3,"name":"Campos Hensley"},{"id":4,"name":"Landry Owen"},{"id":5,"name":"Perez Greer"},{"id":6,"name":"Maldonado Bartlett"},{"id":7,"name":"Hendrix Stein"},{"id":8,"name":"Delgado Wade"},{"id":9,"name":"Rene Wallace"}],"greeting":"Hello, Frazier! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34e17d55284d96d1f5","index":19,"guid":"c7255282-e5a6-4b8b-af24-3b637fd9b044","isActive":false,"balance":"$1,869.10","picture":"http://placehold.it/32x32","age":25,"eyeColor":"blue","name":{"first":"Ochoa","last":"Madden"},"company":"INSURITY","email":"ochoa.madden@insurity.ca","phone":"+1 (934) 422-2951","address":"422 Emerald Street, Sugartown, North Carolina, 2526","about":"Dolor est aliquip pariatur sunt irure deserunt pariatur excepteur do do. Nisi nostrud esse ullamco duis fugiat aliquip cillum. Sint pariatur occaecat ex id aliqua.","registered":"Friday, February 2, 2018 8:53 PM","latitude":"-67.260494","longitude":"76.369882","tags":["aute","minim","commodo","non","ex","qui","ut","veniam","commodo","aliquip"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Lilia Schroeder"},{"id":1,"name":"Amber Meyers"},{"id":2,"name":"Janette Richards"},{"id":3,"name":"Lucinda Key"},{"id":4,"name":"Kristine Freeman"},{"id":5,"name":"Tillman Horne"},{"id":6,"name":"Cervantes Rosario"},{"id":7,"name":"Kerr Mcdonald"},{"id":8,"name":"Dionne Hancock"},{"id":9,"name":"Huber Hester"}],"greeting":"Hello, Ochoa! You have 6 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa3473ba36f5b47cb4fe","index":20,"guid":"9019a995-b226-4d0e-8dbc-c6dc102d3bcc","isActive":true,"balance":"$1,109.74","picture":"http://placehold.it/32x32","age":27,"eyeColor":"green","name":{"first":"Janie","last":"Hendricks"},"company":"TELEQUIET","email":"janie.hendricks@telequiet.info","phone":"+1 (830) 552-3242","address":"302 Dunham Place, Cataract, South Dakota, 2071","about":"Ea et eiusmod adipisicing labore minim. Veniam nostrud non esse non adipisicing aliquip exercitation incididunt laboris velit occaecat Lorem nisi. Veniam adipisicing veniam magna nulla qui in irure culpa. Sint est aute anim sit magna velit nulla labore eu. Mollit duis amet ea culpa amet exercitation officia. Exercitation pariatur deserunt consequat magna enim occaecat mollit sunt incididunt. Fugiat id non veniam elit in ipsum duis exercitation Lorem non culpa in occaecat.","registered":"Monday, February 29, 2016 4:08 PM","latitude":"52.718238","longitude":"-107.958872","tags":["nulla","aliquip","aliquip","reprehenderit","laboris","non","exercitation","anim","nisi","eiusmod"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Diann Robertson"},{"id":1,"name":"Forbes Molina"},{"id":2,"name":"Phelps Medina"},{"id":3,"name":"Billie James"},{"id":4,"name":"Addie Roach"},{"id":5,"name":"Vickie Petersen"},{"id":6,"name":"Navarro Sanford"},{"id":7,"name":"Mclean Sampson"},{"id":8,"name":"Barlow Bolton"},{"id":9,"name":"Nicholson Zamora"}],"greeting":"Hello, Janie! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa3485ccf4bd0ca5238a","index":21,"guid":"ca0b5dc7-21a3-4edb-849e-1d6d81c57f8a","isActive":false,"balance":"$3,102.46","picture":"http://placehold.it/32x32","age":28,"eyeColor":"blue","name":{"first":"Good","last":"Hays"},"company":"GYNKO","email":"good.hays@gynko.me","phone":"+1 (936) 448-2745","address":"538 Lott Place, Rivera, Pennsylvania, 7526","about":"Aliqua veniam aliqua adipisicing aliquip sit aute commodo. Fugiat ut esse laboris duis nisi ipsum ea dolor labore sit pariatur laborum officia. Ipsum ad do tempor nostrud in irure enim ad amet ex id. Nostrud dolore voluptate nulla cupidatat laboris minim.","registered":"Friday, December 9, 2016 10:14 AM","latitude":"-26.185693","longitude":"68.079701","tags":["laborum","dolor","cillum","dolor","duis","in","veniam","enim","aliquip","incididunt"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Erika Dalton"},{"id":1,"name":"Josie Cole"},{"id":2,"name":"Katelyn Dominguez"},{"id":3,"name":"Jasmine Mathis"},{"id":4,"name":"Phoebe Reynolds"},{"id":5,"name":"Bishop Barr"},{"id":6,"name":"Morton Flynn"},{"id":7,"name":"Brock Berger"},{"id":8,"name":"Dominique Obrien"},{"id":9,"name":"Ayers Garrison"}],"greeting":"Hello, Good! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa348b68911a5e827ca8","index":22,"guid":"e0df62b2-c28f-40bb-a00e-f95a667b3fa0","isActive":true,"balance":"$3,544.61","picture":"http://placehold.it/32x32","age":31,"eyeColor":"brown","name":{"first":"Golden","last":"Reid"},"company":"KIDGREASE","email":"golden.reid@kidgrease.org","phone":"+1 (882) 440-2209","address":"952 Holly Street, Monument, Minnesota, 7297","about":"Exercitation esse ullamco do ullamco aliquip eiusmod adipisicing proident. Labore do cupidatat in irure proident. Mollit velit fugiat voluptate aliqua qui voluptate laboris aliqua minim laborum sunt amet. Consequat magna duis commodo ea cupidatat ea culpa eiusmod ut cillum ut eu velit aute. Laboris amet ea irure fugiat nisi amet commodo labore officia excepteur dolor tempor mollit magna. Non consequat dolor reprehenderit ullamco dolor tempor quis voluptate magna ad reprehenderit adipisicing incididunt. Velit laboris elit esse nulla quis.","registered":"Monday, February 24, 2014 11:59 AM","latitude":"30.725621","longitude":"-158.551407","tags":["sit","pariatur","nulla","quis","nisi","ea","qui","deserunt","officia","aliquip"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Bradshaw Gilmore"},{"id":1,"name":"Pugh Munoz"},{"id":2,"name":"Nunez Hawkins"},{"id":3,"name":"Julia Washington"},{"id":4,"name":"Rivers Webb"},{"id":5,"name":"Tara Summers"},{"id":6,"name":"Cheryl Odonnell"},{"id":7,"name":"Theresa Williamson"},{"id":8,"name":"Lesley Duke"},{"id":9,"name":"Snyder Bush"}],"greeting":"Hello, Golden! You have 6 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34fc10000370445e6f","index":23,"guid":"22bcb41a-1eb7-458b-80e7-7428f316d8f9","isActive":false,"balance":"$1,233.98","picture":"http://placehold.it/32x32","age":25,"eyeColor":"green","name":{"first":"Green","last":"Mcknight"},"company":"BESTO","email":"green.mcknight@besto.tv","phone":"+1 (817) 590-3548","address":"672 Montauk Court, Grahamtown, Florida, 6020","about":"Non commodo non nisi cillum fugiat. Quis ad fugiat eu ad consequat veniam adipisicing dolor. Non aliqua nulla Lorem id culpa anim veniam. Aliqua ea et eiusmod minim ut. Id ullamco qui sit aute reprehenderit nulla fugiat velit magna nisi eiusmod sit minim.","registered":"Saturday, February 10, 2018 8:53 AM","latitude":"52.299418","longitude":"21.885332","tags":["incididunt","quis","ea","cupidatat","quis","irure","qui","tempor","voluptate","et"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Mercedes Lowe"},{"id":1,"name":"Chen Perry"},{"id":2,"name":"Maxine Price"},{"id":3,"name":"Bryant Koch"},{"id":4,"name":"Benita Adams"},{"id":5,"name":"Clara Winters"},{"id":6,"name":"Alta Cantu"},{"id":7,"name":"Freida Petty"},{"id":8,"name":"Kimberly Bender"},{"id":9,"name":"Mccormick Roberts"}],"greeting":"Hello, Green! You have 8 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa3489c9bb951ae47da8","index":24,"guid":"22a0295f-7b80-4f06-9fc3-be1cdc79bdfb","isActive":false,"balance":"$2,006.10","picture":"http://placehold.it/32x32","age":35,"eyeColor":"brown","name":{"first":"Hilda","last":"Finch"},"company":"CODAX","email":"hilda.finch@codax.io","phone":"+1 (827) 575-2563","address":"395 Seaview Avenue, Hemlock, Connecticut, 5299","about":"Quis ex cillum ipsum id. Lorem ea velit minim ipsum ipsum irure excepteur nisi culpa officia dolore aliqua dolor deserunt. Et esse aliquip ex enim nisi excepteur mollit tempor non tempor in eu.","registered":"Tuesday, October 24, 2017 6:47 PM","latitude":"-50.758977","longitude":"-133.236945","tags":["et","cillum","elit","aliquip","amet","quis","enim","dolor","eiusmod","adipisicing"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Erma Robbins"},{"id":1,"name":"Gwen Evans"},{"id":2,"name":"Connie Cummings"},{"id":3,"name":"Bonnie Valenzuela"},{"id":4,"name":"Martin Bradford"},{"id":5,"name":"Mcmahon Long"},{"id":6,"name":"Trisha Vaughan"},{"id":7,"name":"Allison West"},{"id":8,"name":"Chapman Olsen"},{"id":9,"name":"Minerva Reeves"}],"greeting":"Hello, Hilda! You have 7 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa340fc9dc39242531ff","index":25,"guid":"3d78eba2-0b29-46e1-8119-b2b170f4be97","isActive":true,"balance":"$2,534.73","picture":"http://placehold.it/32x32","age":21,"eyeColor":"blue","name":{"first":"Cora","last":"Franco"},"company":"ENTHAZE","email":"cora.franco@enthaze.name","phone":"+1 (916) 530-2675","address":"217 Beaver Street, Bergoo, Montana, 7013","about":"Amet anim ipsum velit anim magna incididunt proident excepteur laborum ut voluptate voluptate ex cillum. Labore ea amet eu exercitation excepteur magna ex cupidatat laborum. Nisi ipsum id ex reprehenderit eu enim reprehenderit adipisicing. Ullamco eu sint anim et aute. Laborum sit duis quis sit non duis. Enim minim irure laboris veniam incididunt sint cupidatat.","registered":"Thursday, July 16, 2015 4:42 PM","latitude":"-26.445079","longitude":"77.502053","tags":["id","culpa","consequat","deserunt","velit","sint","dolor","ad","nulla","do"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Barnett Jacobson"},{"id":1,"name":"Lynn Mcpherson"},{"id":2,"name":"Beverley Phelps"},{"id":3,"name":"Aguilar Valdez"},{"id":4,"name":"Sweeney Sandoval"},{"id":5,"name":"Potts Allison"},{"id":6,"name":"Chavez Norman"},{"id":7,"name":"Katherine Torres"},{"id":8,"name":"Kelli Russo"},{"id":9,"name":"Joni Rojas"}],"greeting":"Hello, Cora! You have 9 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa34a34dd832ac2eda38","index":26,"guid":"3ac7f2d1-cbc7-404b-915b-9984bc0947bb","isActive":true,"balance":"$3,667.55","picture":"http://placehold.it/32x32","age":29,"eyeColor":"green","name":{"first":"Faye","last":"Guzman"},"company":"CIPROMOX","email":"faye.guzman@cipromox.biz","phone":"+1 (912) 528-3635","address":"633 Livonia Avenue, Gracey, Mississippi, 9805","about":"Id ea duis velit enim qui esse non sunt. Est ad laboris proident et consequat exercitation. Aliquip do exercitation ea officia mollit dolore. Sit ullamco anim anim minim cillum pariatur officia. Exercitation velit ullamco ullamco sint amet voluptate commodo nulla ipsum qui. Elit cillum mollit ut nulla aute aliquip anim consequat ipsum Lorem sunt nisi nisi est.","registered":"Thursday, February 4, 2016 12:04 AM","latitude":"-23.278189","longitude":"-136.239491","tags":["culpa","consequat","Lorem","eiusmod","deserunt","officia","elit","cillum","aliquip","ex"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Cassandra Forbes"},{"id":1,"name":"Poole Mueller"},{"id":2,"name":"Danielle Espinoza"},{"id":3,"name":"Gentry Leach"},{"id":4,"name":"Allen Berg"},{"id":5,"name":"Rhea Fitzgerald"},{"id":6,"name":"Blair Arnold"},{"id":7,"name":"Felicia Fulton"},{"id":8,"name":"Karla Rodgers"},{"id":9,"name":"Alyssa Mcfadden"}],"greeting":"Hello, Faye! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34e0f6838816113db8","index":27,"guid":"ca5a4646-8edc-4443-91cf-c5dc73927e7a","isActive":false,"balance":"$3,344.58","picture":"http://placehold.it/32x32","age":32,"eyeColor":"brown","name":{"first":"Ryan","last":"Mcintyre"},"company":"STOCKPOST","email":"ryan.mcintyre@stockpost.us","phone":"+1 (872) 474-3518","address":"272 Barwell Terrace, Tedrow, Hawaii, 5053","about":"Incididunt incididunt commodo nulla eiusmod ullamco velit ipsum ad minim non magna culpa laborum. Exercitation ipsum culpa sint in ipsum qui exercitation eiusmod nulla ex. Magna ex eu officia magna esse. Irure dolore anim aute anim tempor nulla adipisicing culpa culpa voluptate ullamco commodo. Adipisicing pariatur excepteur ad reprehenderit reprehenderit dolor magna velit est. Fugiat laboris laborum in et consectetur laborum elit adipisicing.","registered":"Saturday, April 11, 2015 11:23 PM","latitude":"76.874677","longitude":"94.777546","tags":["id","veniam","ad","do","commodo","amet","tempor","sunt","ullamco","laborum"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Vera Olson"},{"id":1,"name":"West Daniel"},{"id":2,"name":"Socorro Manning"},{"id":3,"name":"Louise Haynes"},{"id":4,"name":"Lela Mann"},{"id":5,"name":"Spence Lyons"},{"id":6,"name":"Wynn Gillespie"},{"id":7,"name":"Lynch Jones"},{"id":8,"name":"Patrica Carlson"},{"id":9,"name":"Marietta Hickman"}],"greeting":"Hello, Ryan! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa344299323300eee507","index":28,"guid":"58a7ace0-c6be-4581-bee8-74a5d60398f7","isActive":false,"balance":"$3,743.38","picture":"http://placehold.it/32x32","age":27,"eyeColor":"blue","name":{"first":"Mcgowan","last":"Mcgowan"},"company":"QUINTITY","email":"mcgowan.mcgowan@quintity.biz","phone":"+1 (881) 485-2266","address":"822 Ridge Court, Somerset, Rhode Island, 458","about":"Laboris in velit voluptate amet cupidatat sit. Ea culpa cupidatat in dolor aute ex dolor ut dolore aliqua aliquip cillum. Ipsum laboris sint dolor esse deserunt amet exercitation sunt mollit.","registered":"Tuesday, June 28, 2016 6:36 AM","latitude":"-25.599677","longitude":"84.225442","tags":["nostrud","consectetur","labore","ex","anim","fugiat","nostrud","sunt","deserunt","in"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Steele Riggs"},{"id":1,"name":"Carmella Moran"},{"id":2,"name":"Allyson Blake"},{"id":3,"name":"Carol Cooke"},{"id":4,"name":"Heath Knight"},{"id":5,"name":"Ware Oconnor"},{"id":6,"name":"Osborne Jackson"},{"id":7,"name":"Lilian Burns"},{"id":8,"name":"Mallory Chen"},{"id":9,"name":"Liza Nunez"}],"greeting":"Hello, Mcgowan! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34ad8457b4672e26a6","index":29,"guid":"af06ab7e-2514-4610-805b-9ef1ab10d1b6","isActive":false,"balance":"$2,641.66","picture":"http://placehold.it/32x32","age":40,"eyeColor":"green","name":{"first":"Cathryn","last":"Norris"},"company":"CUIZINE","email":"cathryn.norris@cuizine.com","phone":"+1 (886) 433-3723","address":"631 Hinsdale Street, Edmund, Texas, 818","about":"Enim consectetur eu nulla aliqua enim deserunt veniam deserunt. Commodo dolor anim aliqua anim cupidatat incididunt ex. Consequat qui ex dolor do do ullamco officia laboris ipsum. Sit sunt officia dolore ut dolore veniam dolor Lorem dolor do et qui velit pariatur. Nisi eiusmod ut do duis Lorem cillum do cillum amet dolore. Qui esse officia veniam incididunt consectetur id nostrud deserunt dolor adipisicing ipsum aliquip occaecat. Cillum aliquip sint eu minim aliquip nostrud eiusmod ullamco esse mollit sit amet amet ipsum.","registered":"Wednesday, July 5, 2017 9:58 PM","latitude":"-54.903831","longitude":"-75.383676","tags":["non","minim","fugiat","sint","deserunt","quis","fugiat","do","deserunt","occaecat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Hale Wiley"},{"id":1,"name":"Clarice Holcomb"},{"id":2,"name":"Arlene Rowland"},{"id":3,"name":"Mckee Craig"},{"id":4,"name":"Leann Mullins"},{"id":5,"name":"Lydia Carpenter"},{"id":6,"name":"Franklin Bishop"},{"id":7,"name":"Meadows Rice"},{"id":8,"name":"Lee Bullock"},{"id":9,"name":"Moon Vance"}],"greeting":"Hello, Cathryn! You have 8 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa3438902775803a72d7","index":30,"guid":"0433ad5c-c75b-4e57-b81b-d6f7b4c6ce23","isActive":true,"balance":"$1,413.96","picture":"http://placehold.it/32x32","age":35,"eyeColor":"green","name":{"first":"Gladys","last":"Luna"},"company":"VORTEXACO","email":"gladys.luna@vortexaco.co.uk","phone":"+1 (841) 419-2692","address":"710 Grove Place, Gouglersville, North Dakota, 7911","about":"Adipisicing excepteur incididunt eu ullamco in tempor. Fugiat aute ex veniam culpa labore quis anim magna. Aliqua exercitation culpa cupidatat do ullamco nostrud id. Dolore nisi laborum ullamco fugiat eiusmod laboris exercitation dolore magna id ipsum. Non velit commodo amet fugiat esse cillum tempor ex esse et aliquip ipsum nostrud. Consectetur adipisicing nisi voluptate exercitation amet. Incididunt fugiat dolore elit consequat laboris sunt culpa quis ullamco occaecat consequat enim.","registered":"Thursday, February 25, 2016 1:35 AM","latitude":"-17.405117","longitude":"-94.797563","tags":["ullamco","cupidatat","consectetur","sit","sit","incididunt","voluptate","eu","occaecat","commodo"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Jill Black"},{"id":1,"name":"Myra Wilcox"},{"id":2,"name":"Rojas Mcintosh"},{"id":3,"name":"Duffy Tyson"},{"id":4,"name":"Nettie Payne"},{"id":5,"name":"Pam Avery"},{"id":6,"name":"Guerrero Drake"},{"id":7,"name":"Dunlap Fuller"},{"id":8,"name":"Mercer Hopper"},{"id":9,"name":"Mayra Ramos"}],"greeting":"Hello, Gladys! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34fab19bc37260045d","index":31,"guid":"fbd1f589-514f-4124-a008-6fafdfceebca","isActive":false,"balance":"$2,778.78","picture":"http://placehold.it/32x32","age":29,"eyeColor":"blue","name":{"first":"Mildred","last":"Farrell"},"company":"GLEAMINK","email":"mildred.farrell@gleamink.ca","phone":"+1 (979) 492-2298","address":"285 Lewis Avenue, Toftrees, West Virginia, 2663","about":"Lorem ut incididunt Lorem commodo ipsum nisi sunt consequat officia sit nulla labore nostrud labore. Culpa cupidatat id proident nostrud dolor officia ullamco non veniam. Laborum exercitation amet amet sint. Excepteur excepteur dolor irure laborum. Commodo ut minim incididunt tempor officia consequat occaecat.","registered":"Monday, July 11, 2016 4:58 PM","latitude":"-22.275242","longitude":"138.432225","tags":["dolor","officia","dolor","sit","est","ea","enim","eiusmod","magna","incididunt"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Rodgers Thornton"},{"id":1,"name":"Samantha Stanley"},{"id":2,"name":"Lelia Bowen"},{"id":3,"name":"Herminia Hutchinson"},{"id":4,"name":"Wright Holloway"},{"id":5,"name":"Ava Vinson"},{"id":6,"name":"Francine Schmidt"},{"id":7,"name":"Frederick Bowman"},{"id":8,"name":"Daniel Brown"},{"id":9,"name":"Renee Gill"}],"greeting":"Hello, Mildred! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34a398b521eb072327","index":32,"guid":"045c43e8-f6f2-4490-8d26-3fb124098c34","isActive":false,"balance":"$3,371.61","picture":"http://placehold.it/32x32","age":23,"eyeColor":"blue","name":{"first":"Angelina","last":"Hahn"},"company":"QUANTALIA","email":"angelina.hahn@quantalia.info","phone":"+1 (813) 431-3542","address":"894 Louisa Street, Cochranville, Maine, 2019","about":"Tempor Lorem labore et officia id. Laboris aliqua Lorem do aliquip dolore officia quis Lorem quis et qui voluptate sunt. Id Lorem voluptate elit ea dolor occaecat magna occaecat et ad sint. Cillum mollit incididunt nisi reprehenderit Lorem duis reprehenderit minim voluptate exercitation incididunt est nisi esse.","registered":"Monday, December 12, 2016 7:40 AM","latitude":"28.524161","longitude":"97.447644","tags":["pariatur","incididunt","est","ut","pariatur","laboris","duis","nulla","eu","fugiat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Sylvia Bradley"},{"id":1,"name":"Jamie Wells"},{"id":2,"name":"Jodie Mccormick"},{"id":3,"name":"Calderon Hansen"},{"id":4,"name":"Fuentes Walton"},{"id":5,"name":"Lacy Goff"},{"id":6,"name":"Marguerite Cantrell"},{"id":7,"name":"Goldie Best"},{"id":8,"name":"Melva Ware"},{"id":9,"name":"Lina Stuart"}],"greeting":"Hello, Angelina! You have 6 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa3415690121e1d31dc8","index":33,"guid":"1c4d9d82-a2ed-4137-bbb2-1e31c25c3845","isActive":true,"balance":"$1,955.48","picture":"http://placehold.it/32x32","age":38,"eyeColor":"green","name":{"first":"Rosanna","last":"Morgan"},"company":"AQUOAVO","email":"rosanna.morgan@aquoavo.me","phone":"+1 (972) 555-2633","address":"125 Downing Street, Lutsen, Missouri, 7484","about":"Officia officia magna exercitation do culpa nostrud anim minim reprehenderit velit exercitation exercitation excepteur. Cillum est occaecat id nisi. Ipsum dolor quis incididunt non anim. Officia in esse nulla do commodo eu non commodo consequat. Et proident et id officia fugiat consectetur do non. Est ea eu velit fugiat ea nostrud id consequat cillum sit consectetur. Proident ut Lorem incididunt Lorem aliqua mollit Lorem dolore esse mollit elit eu.","registered":"Saturday, November 11, 2017 6:19 AM","latitude":"-26.09161","longitude":"-142.715019","tags":["veniam","exercitation","proident","minim","aliquip","labore","sint","quis","laboris","ut"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Durham Mayo"},{"id":1,"name":"Puckett Clayton"},{"id":2,"name":"Guzman Franks"},{"id":3,"name":"Marion Riddle"},{"id":4,"name":"Bowman Clemons"},{"id":5,"name":"Church Dudley"},{"id":6,"name":"Adeline Gross"},{"id":7,"name":"Wolf Boyle"},{"id":8,"name":"Maggie Fry"},{"id":9,"name":"Molly Willis"}],"greeting":"Hello, Rosanna! You have 9 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa3473b934c37662ee87","index":34,"guid":"9e975a37-6c04-4476-8af8-a8da71f95bce","isActive":false,"balance":"$2,374.83","picture":"http://placehold.it/32x32","age":26,"eyeColor":"green","name":{"first":"Alexis","last":"Stephens"},"company":"VIDTO","email":"alexis.stephens@vidto.org","phone":"+1 (823) 415-2326","address":"129 Cove Lane, Loretto, Federated States Of Micronesia, 4132","about":"Ipsum laboris elit irure sunt velit minim et aliquip qui labore. Cupidatat pariatur cupidatat labore dolore ullamco nisi ipsum aute. Aute fugiat pariatur cupidatat fugiat irure do proident voluptate pariatur duis quis sit est. Ex ipsum ea ipsum nulla nostrud ad veniam sunt cupidatat aliquip occaecat elit est voluptate. Nostrud non enim quis consequat cupidatat voluptate occaecat veniam ad sunt sint Lorem tempor minim.","registered":"Tuesday, February 9, 2016 6:58 AM","latitude":"30.265793","longitude":"133.438276","tags":["veniam","eiusmod","Lorem","officia","consequat","dolor","tempor","sunt","ex","sit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Fanny Atkinson"},{"id":1,"name":"Maryanne Conrad"},{"id":2,"name":"Catalina Harmon"},{"id":3,"name":"Cooke Rowe"},{"id":4,"name":"Angie Mcgee"},{"id":5,"name":"Stokes Gibson"},{"id":6,"name":"Jones Vega"},{"id":7,"name":"Leah Butler"},{"id":8,"name":"Taylor Peters"},{"id":9,"name":"Cochran Neal"}],"greeting":"Hello, Alexis! You have 10 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa34629b7e6a2d9be431","index":35,"guid":"3c9958e7-c013-4af9-8fcd-b6956105558b","isActive":false,"balance":"$3,908.65","picture":"http://placehold.it/32x32","age":32,"eyeColor":"green","name":{"first":"Aguirre","last":"Mckinney"},"company":"NETPLODE","email":"aguirre.mckinney@netplode.tv","phone":"+1 (894) 423-3860","address":"403 Glendale Court, Wildwood, Massachusetts, 8080","about":"Aliqua elit sunt anim magna aliquip ullamco non incididunt. Proident irure Lorem excepteur aliqua deserunt nisi in ea reprehenderit. Ad laboris aute cillum ut exercitation culpa dolor.","registered":"Wednesday, May 7, 2014 2:56 PM","latitude":"-76.365309","longitude":"-157.854137","tags":["proident","deserunt","est","sint","sint","magna","aliquip","in","commodo","velit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Dorsey Cote"},{"id":1,"name":"Levy Castro"},{"id":2,"name":"Anne Love"},{"id":3,"name":"Neva Suarez"},{"id":4,"name":"Tammie Gallagher"},{"id":5,"name":"Kristi Parker"},{"id":6,"name":"Waller Miller"},{"id":7,"name":"Nadia Wood"},{"id":8,"name":"Powell Lott"},{"id":9,"name":"Chris Hoffman"}],"greeting":"Hello, Aguirre! You have 7 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa3426ab5d444b6a10f9","index":36,"guid":"8b4fdf0f-9db7-41d4-b39d-139cd785d7b5","isActive":true,"balance":"$2,280.91","picture":"http://placehold.it/32x32","age":30,"eyeColor":"green","name":{"first":"Richards","last":"Snow"},"company":"BLURRYBUS","email":"richards.snow@blurrybus.io","phone":"+1 (905) 463-2264","address":"494 Brightwater Court, Marenisco, Colorado, 1830","about":"Mollit duis nisi eu aliquip tempor irure laborum ad minim aliqua. Ullamco voluptate enim adipisicing consectetur adipisicing occaecat tempor voluptate dolor incididunt tempor fugiat. Adipisicing cupidatat esse ex commodo nostrud officia cupidatat ullamco magna proident incididunt. Adipisicing sint Lorem qui duis laborum non consequat minim incididunt sint quis in voluptate. Minim ea ex ex cupidatat consectetur aliqua proident exercitation cupidatat duis sint ut. Magna enim eu aute aliquip est minim nulla mollit. Occaecat magna ad nisi eiusmod aliqua deserunt consectetur exercitation adipisicing commodo voluptate.","registered":"Thursday, October 13, 2016 7:34 PM","latitude":"-84.839295","longitude":"-173.645597","tags":["ut","veniam","ullamco","culpa","exercitation","fugiat","quis","consequat","amet","nostrud"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Lakeisha Banks"},{"id":1,"name":"Anderson Wooten"},{"id":2,"name":"Marie Hopkins"},{"id":3,"name":"Parks Jensen"},{"id":4,"name":"Wendi Page"},{"id":5,"name":"Hart Andrews"},{"id":6,"name":"Margery Irwin"},{"id":7,"name":"Harrison Kirby"},{"id":8,"name":"Ivy Santiago"},{"id":9,"name":"Tamra Witt"}],"greeting":"Hello, Richards! You have 5 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa345399675c8e97e073","index":37,"guid":"8c6ce068-927a-4c5a-ba09-bc264d9fd709","isActive":true,"balance":"$2,124.25","picture":"http://placehold.it/32x32","age":25,"eyeColor":"brown","name":{"first":"Penny","last":"Swanson"},"company":"PLAYCE","email":"penny.swanson@playce.name","phone":"+1 (874) 539-3904","address":"797 Pershing Loop, Gorst, New York, 2236","about":"Cillum ad irure consequat do cillum culpa aliquip exercitation est cupidatat ex do consectetur aute. Enim proident ea velit dolore ex ea tempor elit consequat laboris. Laborum velit eu minim irure nostrud cupidatat mollit minim nostrud. Tempor ea laboris nostrud tempor veniam. Cillum ex in cupidatat ut laboris. Fugiat nisi in quis velit eu fugiat veniam aliquip aliqua anim est.","registered":"Tuesday, September 13, 2016 10:26 AM","latitude":"46.06826","longitude":"-179.707892","tags":["ea","proident","ea","culpa","consequat","incididunt","incididunt","non","ea","duis"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Webster Alford"},{"id":1,"name":"Stacey Boyd"},{"id":2,"name":"Nellie Delgado"},{"id":3,"name":"Baker Slater"},{"id":4,"name":"Moody Underwood"},{"id":5,"name":"Rosales Buck"},{"id":6,"name":"Hess Todd"},{"id":7,"name":"Lane Farley"},{"id":8,"name":"Smith Sears"},{"id":9,"name":"Newman Caldwell"}],"greeting":"Hello, Penny! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa344f3de72033606554","index":38,"guid":"78c70b05-ba42-4c24-a841-3cb06a29112c","isActive":false,"balance":"$2,845.79","picture":"http://placehold.it/32x32","age":26,"eyeColor":"green","name":{"first":"Christi","last":"French"},"company":"SKYPLEX","email":"christi.french@skyplex.biz","phone":"+1 (831) 500-2423","address":"381 Colby Court, Haena, Marshall Islands, 839","about":"Consectetur magna in fugiat quis aute anim elit officia ex sint exercitation. Sunt exercitation pariatur enim laborum mollit labore. Minim enim pariatur enim ex ipsum eiusmod est quis irure.","registered":"Wednesday, August 17, 2016 9:32 PM","latitude":"-70.652626","longitude":"-160.945222","tags":["dolor","reprehenderit","nulla","laborum","esse","est","nostrud","velit","cupidatat","id"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Haley Mcneil"},{"id":1,"name":"Bernadine Bean"},{"id":2,"name":"Agnes Kirk"},{"id":3,"name":"Margie Aguirre"},{"id":4,"name":"Mitzi Harper"},{"id":5,"name":"Hatfield Marks"},{"id":6,"name":"Rachael Harris"},{"id":7,"name":"Serena Sykes"},{"id":8,"name":"Davis Cline"},{"id":9,"name":"Blanca Cameron"}],"greeting":"Hello, Christi! You have 7 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa346c214ead2d9abdf1","index":39,"guid":"eae6507c-4bae-4729-9212-aa5a8c48006b","isActive":false,"balance":"$3,360.83","picture":"http://placehold.it/32x32","age":26,"eyeColor":"green","name":{"first":"Kidd","last":"Stark"},"company":"CENTREXIN","email":"kidd.stark@centrexin.us","phone":"+1 (832) 581-2526","address":"759 Blake Court, Turpin, Oklahoma, 7285","about":"Dolore dolor labore ut aliquip ad enim do enim do. Fugiat voluptate culpa qui ut officia consequat ipsum laboris ullamco exercitation voluptate consectetur proident minim. Laboris nisi amet quis aliquip adipisicing eiusmod pariatur in ut quis.","registered":"Saturday, April 30, 2016 2:38 AM","latitude":"-66.122491","longitude":"178.936101","tags":["pariatur","duis","qui","qui","deserunt","minim","in","aliqua","voluptate","laboris"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Petty Ferrell"},{"id":1,"name":"Jacqueline Reilly"},{"id":2,"name":"Robles Frye"},{"id":3,"name":"Woods Weeks"},{"id":4,"name":"Louisa Levine"},{"id":5,"name":"Hardin Farmer"},{"id":6,"name":"Roach Glass"},{"id":7,"name":"Jacobs Faulkner"},{"id":8,"name":"Oconnor Hurley"},{"id":9,"name":"Loretta Patel"}],"greeting":"Hello, Kidd! You have 9 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa348ff4bb496eae2130","index":40,"guid":"77f38c4e-206a-41ad-b8ec-8c124da29d71","isActive":true,"balance":"$1,227.03","picture":"http://placehold.it/32x32","age":39,"eyeColor":"green","name":{"first":"Angelita","last":"Mills"},"company":"CABLAM","email":"angelita.mills@cablam.biz","phone":"+1 (977) 596-3341","address":"240 Court Street, Collins, South Carolina, 3358","about":"Quis velit labore adipisicing amet excepteur. Exercitation enim adipisicing adipisicing ut nostrud cillum. Est anim duis duis voluptate Lorem occaecat. Ut commodo laborum reprehenderit aliqua dolor eiusmod cupidatat duis. Deserunt et mollit velit excepteur qui non deserunt esse ut tempor ut voluptate. Sit culpa sit officia Lorem nisi ad nulla amet ipsum elit dolore. Mollit amet id Lorem magna esse consequat duis irure ad veniam ea duis.","registered":"Saturday, October 11, 2014 3:14 PM","latitude":"47.351007","longitude":"105.90419","tags":["sunt","non","voluptate","consectetur","ut","est","laborum","ad","cupidatat","esse"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Langley Powers"},{"id":1,"name":"Carson Woodard"},{"id":2,"name":"Peterson Pugh"},{"id":3,"name":"Lorie Perez"},{"id":4,"name":"Case Parks"},{"id":5,"name":"Elizabeth Boone"},{"id":6,"name":"Marian Maddox"},{"id":7,"name":"Mara Sargent"},{"id":8,"name":"Gilliam Garner"},{"id":9,"name":"Weeks Ross"}],"greeting":"Hello, Angelita! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34e7a9b39ca8267980","index":41,"guid":"2a508c71-aa4d-4f9e-a96d-3f6495bf102c","isActive":true,"balance":"$2,610.88","picture":"http://placehold.it/32x32","age":34,"eyeColor":"green","name":{"first":"Kirk","last":"Conner"},"company":"ECRATER","email":"kirk.conner@ecrater.com","phone":"+1 (876) 408-2160","address":"984 Erasmus Street, Ronco, Tennessee, 2168","about":"Nisi sint velit reprehenderit ex anim in veniam elit laborum aliquip. Sit exercitation mollit magna veniam officia minim ipsum labore dolore nulla. Dolor ad adipisicing magna amet aliquip. Amet elit aliqua dolore dolore mollit ex veniam sit aliquip proident pariatur duis aliqua do. Eu et id aute in eu qui minim sunt ullamco quis ea ipsum. Elit ullamco dolor irure culpa proident tempor enim irure irure deserunt anim ex. Consequat proident Lorem enim est.","registered":"Tuesday, June 20, 2017 5:36 PM","latitude":"9.0713","longitude":"-13.535663","tags":["eu","duis","deserunt","sint","consequat","magna","laborum","aliquip","anim","ex"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Bridges Floyd"},{"id":1,"name":"Barnes Head"},{"id":2,"name":"Conley Barrett"},{"id":3,"name":"Waters Bennett"},{"id":4,"name":"Kaufman Morton"},{"id":5,"name":"Copeland Rose"},{"id":6,"name":"Kendra Daniels"},{"id":7,"name":"Julie Roth"},{"id":8,"name":"Tami Snider"},{"id":9,"name":"Margaret Mcmillan"}],"greeting":"Hello, Kirk! You have 10 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa341e9858da323f3e8a","index":42,"guid":"8ae8c734-331c-4707-9225-f38615f7d87c","isActive":true,"balance":"$3,280.97","picture":"http://placehold.it/32x32","age":22,"eyeColor":"blue","name":{"first":"Hickman","last":"Langley"},"company":"VERAQ","email":"hickman.langley@veraq.co.uk","phone":"+1 (908) 439-3901","address":"469 Nova Court, Buxton, Wisconsin, 8132","about":"Voluptate et ipsum ea in voluptate. Amet incididunt cupidatat duis veniam ipsum veniam deserunt. Mollit duis exercitation cillum pariatur culpa enim. Excepteur nulla ea velit sunt aliqua. Est velit consectetur in aliqua. Duis pariatur ea sit laborum Lorem quis nisi dolore. Fugiat id ad consectetur irure ipsum adipisicing eu sit duis laboris tempor sunt irure minim.","registered":"Monday, December 19, 2016 6:40 PM","latitude":"38.352256","longitude":"-79.01819","tags":["commodo","laboris","in","esse","incididunt","eiusmod","aute","amet","qui","esse"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Snow Stafford"},{"id":1,"name":"Pollard Fischer"},{"id":2,"name":"Lillian Bowers"},{"id":3,"name":"Katy Lancaster"},{"id":4,"name":"Ana Tillman"},{"id":5,"name":"Tanisha Dean"},{"id":6,"name":"Verna Osborn"},{"id":7,"name":"Lorene Blackwell"},{"id":8,"name":"Klein Lindsay"},{"id":9,"name":"Kay Simpson"}],"greeting":"Hello, Hickman! You have 9 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34745076e981951cad","index":43,"guid":"cf66637a-0f87-43c1-8d3e-8a8eb32a3b82","isActive":true,"balance":"$3,777.58","picture":"http://placehold.it/32x32","age":27,"eyeColor":"blue","name":{"first":"Dorthy","last":"Dixon"},"company":"AVENETRO","email":"dorthy.dixon@avenetro.ca","phone":"+1 (865) 445-2658","address":"845 Hendrix Street, Mathews, Delaware, 8863","about":"Consectetur qui minim tempor proident sint esse est quis. Eiusmod aliqua ea ut officia quis deserunt cillum quis aliqua exercitation ipsum. Magna qui veniam enim ullamco nulla. Occaecat commodo excepteur qui laborum qui ea mollit Lorem non ex nostrud proident reprehenderit nulla. Ipsum laboris irure ullamco do consectetur laboris sunt id adipisicing ullamco duis quis cupidatat.","registered":"Wednesday, January 13, 2016 1:56 AM","latitude":"-45.361089","longitude":"118.758428","tags":["esse","aute","excepteur","esse","labore","ut","irure","ea","cupidatat","aliquip"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Dillon Holman"},{"id":1,"name":"Megan Joyner"},{"id":2,"name":"Casandra Branch"},{"id":3,"name":"Linda Livingston"},{"id":4,"name":"Pope Talley"},{"id":5,"name":"Vasquez Cox"},{"id":6,"name":"Skinner Cross"},{"id":7,"name":"Anna Beasley"},{"id":8,"name":"Dawson Gould"},{"id":9,"name":"Dean Shepard"}],"greeting":"Hello, Dorthy! You have 9 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa34f7481ee6928ebadf","index":44,"guid":"d5e4825b-cbfe-4994-af1a-03a4a46fd290","isActive":false,"balance":"$2,976.38","picture":"http://placehold.it/32x32","age":33,"eyeColor":"brown","name":{"first":"Lynnette","last":"Rosales"},"company":"YURTURE","email":"lynnette.rosales@yurture.info","phone":"+1 (931) 424-3261","address":"293 Rose Street, Alderpoint, Alabama, 2523","about":"Veniam et et laboris irure elit consectetur ut exercitation fugiat. Cillum in eu cillum elit et. Ullamco mollit tempor consectetur excepteur enim laborum amet magna id. Cillum commodo mollit qui consequat eiusmod cupidatat amet. Do labore aute consequat aliqua nostrud ut officia qui elit id labore dolor laboris. Fugiat nostrud ea id officia adipisicing. Incididunt reprehenderit velit sunt elit ex officia nulla nostrud sit culpa pariatur ipsum.","registered":"Monday, June 22, 2015 9:25 PM","latitude":"83.236293","longitude":"-165.695513","tags":["sit","deserunt","non","ipsum","irure","laborum","ut","amet","dolore","Lorem"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Harriet Vasquez"},{"id":1,"name":"Warren Sherman"},{"id":2,"name":"Rita Downs"},{"id":3,"name":"Celia Mcclain"},{"id":4,"name":"Bethany Mason"},{"id":5,"name":"Barker Anderson"},{"id":6,"name":"Lucy Santana"},{"id":7,"name":"Aurelia Dickerson"},{"id":8,"name":"Bullock Durham"},{"id":9,"name":"Middleton Huffman"}],"greeting":"Hello, Lynnette! You have 7 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa3447392bc6d49d3c6a","index":45,"guid":"93bf294e-6516-462c-affc-fefb2fc25cfb","isActive":false,"balance":"$1,251.30","picture":"http://placehold.it/32x32","age":26,"eyeColor":"brown","name":{"first":"Wagner","last":"Murphy"},"company":"NEXGENE","email":"wagner.murphy@nexgene.me","phone":"+1 (861) 488-2141","address":"567 Cleveland Street, Belva, Northern Mariana Islands, 1561","about":"Mollit ut ipsum id qui ex magna duis magna ipsum cillum proident. Veniam eu dolore deserunt nulla amet dolore quis aliqua. Est dolore anim aliqua ex exercitation cupidatat voluptate. Ea ipsum proident sunt non. Minim laborum nisi culpa ex sit excepteur reprehenderit aute exercitation officia veniam. Occaecat est tempor anim incididunt elit incididunt commodo fugiat magna laboris laboris irure. Culpa aliquip ipsum tempor cupidatat non laboris ad anim duis.","registered":"Saturday, June 20, 2015 8:46 AM","latitude":"26.957528","longitude":"-83.855732","tags":["officia","aliqua","et","cupidatat","labore","culpa","nisi","sit","mollit","cupidatat"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Gertrude Mclaughlin"},{"id":1,"name":"Esther Pope"},{"id":2,"name":"Jacquelyn Rush"},{"id":3,"name":"Ellen Keith"},{"id":4,"name":"Zimmerman Mejia"},{"id":5,"name":"Craft Franklin"},{"id":6,"name":"Hubbard Christian"},{"id":7,"name":"Bridgett Kline"},{"id":8,"name":"Gonzalez Douglas"},{"id":9,"name":"Larson Justice"}],"greeting":"Hello, Wagner! You have 10 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa344a1963094ff64da5","index":46,"guid":"6b9e6342-267d-4844-b030-723bae27c4ce","isActive":true,"balance":"$1,943.18","picture":"http://placehold.it/32x32","age":35,"eyeColor":"green","name":{"first":"Tonya","last":"Lowery"},"company":"MYOPIUM","email":"tonya.lowery@myopium.org","phone":"+1 (881) 553-2631","address":"975 Amboy Street, Nipinnawasee, California, 9715","about":"Pariatur dolor exercitation dolore eiusmod et nisi in deserunt laborum nisi magna. Dolor laborum culpa quis aute pariatur labore est velit voluptate. Minim labore occaecat aute fugiat sint laborum voluptate tempor minim.","registered":"Wednesday, December 30, 2015 5:12 AM","latitude":"13.22295","longitude":"159.692667","tags":["eu","veniam","et","est","est","aliquip","sint","non","anim","ex"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Debra Stanton"},{"id":1,"name":"Harrington Carney"},{"id":2,"name":"Helena Rivas"},{"id":3,"name":"Pearl Wagner"},{"id":4,"name":"Bridgette Lawson"},{"id":5,"name":"Patel Sutton"},{"id":6,"name":"Horton Saunders"},{"id":7,"name":"Sherri Nelson"},{"id":8,"name":"Jacobson Hurst"},{"id":9,"name":"Shanna Cotton"}],"greeting":"Hello, Tonya! You have 5 unread messages.","favoriteFruit":"banana"},{"_id":"5ae0aa34b44b3eb5d573dfe4","index":47,"guid":"6fc32a51-1b79-4679-910e-5eb13aa223f8","isActive":true,"balance":"$1,559.68","picture":"http://placehold.it/32x32","age":38,"eyeColor":"green","name":{"first":"Johnnie","last":"Travis"},"company":"SURELOGIC","email":"johnnie.travis@surelogic.tv","phone":"+1 (855) 506-2479","address":"289 Anna Court, Denio, Virgin Islands, 7460","about":"Adipisicing ipsum enim ullamco laborum nostrud nulla esse quis ipsum voluptate eiusmod. Fugiat ut est consectetur nisi minim. Id sint nostrud eiusmod cupidatat fugiat.","registered":"Tuesday, August 1, 2017 5:05 PM","latitude":"-4.213212","longitude":"-93.529387","tags":["est","consequat","consectetur","enim","qui","et","ullamco","ipsum","reprehenderit","mollit"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Ester Harrell"},{"id":1,"name":"Shepard Shelton"},{"id":2,"name":"Enid Romero"},{"id":3,"name":"Effie Baxter"},{"id":4,"name":"Dona Huber"},{"id":5,"name":"Berger Heath"},{"id":6,"name":"Allison Lynn"},{"id":7,"name":"Blackwell Edwards"},{"id":8,"name":"Lottie Chambers"},{"id":9,"name":"Todd Holmes"}],"greeting":"Hello, Johnnie! You have 8 unread messages.","favoriteFruit":"apple"},{"_id":"5ae0aa349fe0e22b1f9fbdfd","index":48,"guid":"3ae5da74-591c-48c9-a328-6ec454619337","isActive":false,"balance":"$2,825.93","picture":"http://placehold.it/32x32","age":20,"eyeColor":"blue","name":{"first":"Deanna","last":"Adkins"},"company":"VIRXO","email":"deanna.adkins@virxo.io","phone":"+1 (981) 554-3807","address":"650 Bridge Street, Mappsville, Kansas, 4759","about":"Et aliqua laboris do excepteur veniam dolor cupidatat nisi eu voluptate sunt eiusmod non eiusmod. Minim eu dolor sunt aute. Nostrud laborum ad voluptate ipsum occaecat Lorem. Dolore sit amet cupidatat enim cillum ipsum cillum. Et laborum ut proident officia mollit. Eu velit commodo esse id sint aliquip Lorem et occaecat aliqua. Consequat eiusmod fugiat cillum consequat eiusmod eiusmod ex qui est duis consequat et officia.","registered":"Tuesday, December 13, 2016 7:50 AM","latitude":"33.223746","longitude":"15.22617","tags":["excepteur","ex","deserunt","qui","sit","aute","nostrud","do","labore","ipsum"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Riddle Reese"},{"id":1,"name":"Selma Henson"},{"id":2,"name":"Yesenia Middleton"},{"id":3,"name":"Nola White"},{"id":4,"name":"Sweet Guerra"},{"id":5,"name":"Terra Small"},{"id":6,"name":"Hamilton Hughes"},{"id":7,"name":"Hester Poole"},{"id":8,"name":"Calhoun Hubbard"},{"id":9,"name":"Bonita Hall"}],"greeting":"Hello, Deanna! You have 10 unread messages.","favoriteFruit":"strawberry"},{"_id":"5ae0aa34e364048f9370e15f","index":49,"guid":"bf614856-f78f-44c2-8abc-0f89064d6c7b","isActive":true,"balance":"$3,625.36","picture":"http://placehold.it/32x32","age":35,"eyeColor":"blue","name":{"first":"Rosella","last":"Zimmerman"},"company":"ZYTREX","email":"rosella.zimmerman@zytrex.name","phone":"+1 (928) 486-3626","address":"915 Dean Street, Valmy, Iowa, 2698","about":"Dolore cupidatat quis ex esse. Mollit labore exercitation exercitation Lorem elit pariatur incididunt culpa nostrud pariatur tempor ut id. Lorem nostrud mollit elit veniam dolore incididunt duis consectetur nostrud proident eu occaecat cupidatat incididunt. Dolor pariatur fugiat sunt do consequat officia sit aliquip officia quis consequat ipsum dolor ad. Occaecat mollit sunt sit occaecat. Quis in irure ipsum sunt. Dolor ullamco sunt dolor labore proident consectetur ex irure est dolor officia.","registered":"Sunday, September 24, 2017 5:22 AM","latitude":"-58.21478","longitude":"2.994387","tags":["id","irure","labore","anim","non","est","eiusmod","aliquip","enim","proident"],"range":[0,1,2,3,4,5,6,7,8,9],"friends":[{"id":0,"name":"Ortiz Rivera"},{"id":1,"name":"Nina Mcclure"},{"id":2,"name":"Donna Whitley"},{"id":3,"name":"Ladonna Salazar"},{"id":4,"name":"Rosario Gay"},{"id":5,"name":"Nelda Rich"},{"id":6,"name":"Harriett Phillips"},{"id":7,"name":"Marianne Leblanc"},{"id":8,"name":"Anastasia Smith"},{"id":9,"name":"Stacy Cochran"}],"greeting":"Hello, Rosella! You have 8 unread messages.","favoriteFruit":"apple"}]}
2 |
--------------------------------------------------------------------------------
/testdata/one.gron:
--------------------------------------------------------------------------------
1 | json = {};
2 | json.abool = true;
3 | json.abool2 = false;
4 | json.five = {};
5 | json.five.alpha = [];
6 | json.five.alpha[0] = "fo";
7 | json.five.alpha[1] = "fum";
8 | json.five.beta = {};
9 | json.five.beta.hey = "How's tricks?";
10 | json.four = [];
11 | json.four[0] = 1;
12 | json.four[1] = 2;
13 | json.four[2] = 3;
14 | json.four[3] = 4;
15 | json.id = 66912849;
16 | json.isnull = null;
17 | json.one = 1;
18 | json.two = 2.2;
19 | json["three-b"] = "3";
20 |
--------------------------------------------------------------------------------
/testdata/one.jgron:
--------------------------------------------------------------------------------
1 | [[],{}]
2 | [["abool"],true]
3 | [["abool2"],false]
4 | [["five"],{}]
5 | [["five","alpha"],[]]
6 | [["five","alpha",0],"fo"]
7 | [["five","alpha",1],"fum"]
8 | [["five","beta"],{}]
9 | [["five","beta","hey"],"How's tricks?"]
10 | [["four"],[]]
11 | [["four",0],1]
12 | [["four",1],2]
13 | [["four",2],3]
14 | [["four",3],4]
15 | [["id"],66912849]
16 | [["isnull"],null]
17 | [["one"],1]
18 | [["two"],2.2]
19 | [["three-b"],"3"]
20 |
--------------------------------------------------------------------------------
/testdata/one.json:
--------------------------------------------------------------------------------
1 | {
2 | "one": 1,
3 | "two": 2.2,
4 | "three-b": "3",
5 | "four": [1,2,3,4],
6 | "five": {
7 | "alpha": ["fo", "fum"],
8 | "beta": {
9 | "hey": "How's tricks?"
10 | }
11 | },
12 | "abool": true,
13 | "abool2": false,
14 | "isnull": null,
15 | "id": 66912849
16 | }
17 |
--------------------------------------------------------------------------------
/testdata/scalar-stream.gron:
--------------------------------------------------------------------------------
1 | json = [];
2 | json[0] = true;
3 | json[1] = false;
4 | json[2] = null;
5 | json[3] = "hello";
6 | json[4] = 4;
7 | json[5] = 4.4;
8 |
--------------------------------------------------------------------------------
/testdata/scalar-stream.jgron:
--------------------------------------------------------------------------------
1 | [[],[]]
2 | [[0],true]
3 | [[1],false]
4 | [[2],null]
5 | [[3],"hello"]
6 | [[4],4]
7 | [[5],4.4]
8 |
--------------------------------------------------------------------------------
/testdata/scalar-stream.json:
--------------------------------------------------------------------------------
1 | true
2 | false
3 | null
4 | "hello"
5 | 4
6 | 4.4
7 |
--------------------------------------------------------------------------------
/testdata/stream.gron:
--------------------------------------------------------------------------------
1 | json = [];
2 | json[0] = {};
3 | json[0].one = 1;
4 | json[0].three = [];
5 | json[0].three[0] = 1;
6 | json[0].three[1] = 2;
7 | json[0].three[2] = 3;
8 | json[0].two = 2;
9 | json[1] = {};
10 | json[1].one = 1;
11 | json[1].three = [];
12 | json[1].three[0] = 1;
13 | json[1].three[1] = 2;
14 | json[1].three[2] = 3;
15 | json[1].two = 2;
16 |
--------------------------------------------------------------------------------
/testdata/stream.jgron:
--------------------------------------------------------------------------------
1 | [[],[]]
2 | [[0],{}]
3 | [[0,"one"],1]
4 | [[0,"three"],[]]
5 | [[0,"three",0],1]
6 | [[0,"three",1],2]
7 | [[0,"three",2],3]
8 | [[0,"two"],2]
9 | [[1],{}]
10 | [[1,"one"],1]
11 | [[1,"three"],[]]
12 | [[1,"three",0],1]
13 | [[1,"three",1],2]
14 | [[1,"three",2],3]
15 | [[1,"two"],2]
16 |
--------------------------------------------------------------------------------
/testdata/stream.json:
--------------------------------------------------------------------------------
1 | {"one": 1, "two": 2, "three": [1, 2, 3]}
2 | {"one": 1, "two": 2, "three": [1, 2, 3]}
3 |
--------------------------------------------------------------------------------
/testdata/three.gron:
--------------------------------------------------------------------------------
1 | json = {};
2 | json.abool = true;
3 | json.abool2 = false;
4 | json.five = {};
5 | json.five.alpha = [];
6 | json.five.alpha[0] = "fo";
7 | json.five.alpha[1] = "fum";
8 | json.five.beta = {};
9 | json.five.beta.hey = "How's tricks?";
10 | json.four = [];
11 | json.four[0] = 1;
12 | json.four[1] = 2;
13 | json.four[2] = 3;
14 | json.four[3] = 4;
15 | json.four[4] = 5;
16 | json.four[5] = 6;
17 | json.four[6] = 7;
18 | json.four[7] = 8;
19 | json.four[8] = 9;
20 | json.four[9] = 10;
21 | json.four[10] = 11;
22 | json.four[11] = 12;
23 | json.four[12] = 13;
24 | json.four[13] = 14;
25 | json.isnull = null;
26 | json.one = 1;
27 | json.two = 2.2;
28 | json.ಠ_ಠ = "yarly";
29 | json["three-b"] = "3";
30 |
--------------------------------------------------------------------------------
/testdata/three.jgron:
--------------------------------------------------------------------------------
1 | [[],{}]
2 | [["abool"],true]
3 | [["abool2"],false]
4 | [["five"],{}]
5 | [["five","alpha"],[]]
6 | [["five","alpha",0],"fo"]
7 | [["five","alpha",1],"fum"]
8 | [["five","beta"],{}]
9 | [["five","beta","hey"],"How's tricks?"]
10 | [["four"],[]]
11 | [["four",0],1]
12 | [["four",1],2]
13 | [["four",2],3]
14 | [["four",3],4]
15 | [["four",4],5]
16 | [["four",5],6]
17 | [["four",6],7]
18 | [["four",7],8]
19 | [["four",8],9]
20 | [["four",9],10]
21 | [["four",10],11]
22 | [["four",11],12]
23 | [["four",12],13]
24 | [["four",13],14]
25 | [["isnull"],null]
26 | [["one"],1]
27 | [["two"],2.2]
28 | [["ಠ_ಠ"],"yarly"]
29 | [["three-b"],"3"]
30 |
--------------------------------------------------------------------------------
/testdata/three.json:
--------------------------------------------------------------------------------
1 | {
2 | "one": 1,
3 | "two": 2.2,
4 | "three-b": "3",
5 | "four": [1,2,3,4,5,6,7,8,9,10,11,12,13,14],
6 | "five": {
7 | "alpha": ["fo", "fum"],
8 | "beta": {
9 | "hey": "How's tricks?"
10 | }
11 | },
12 | "abool": true,
13 | "abool2": false,
14 | "isnull": null,
15 | "ಠ_ಠ": "yarly"
16 | }
17 |
--------------------------------------------------------------------------------
/testdata/two-b.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Tom",
3 | "github": "https://github.com/tomnomnom/",
4 | "likes": ["code", "cheese", "meat"],
5 | "contact": {
6 | "email": "contact@tomnomnom.com",
7 | "twitter": "@TomNomNom"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/testdata/two.gron:
--------------------------------------------------------------------------------
1 | json = {};
2 | json.contact = {};
3 | json.contact.email = "mail@tomnomnom.com";
4 | json.contact.twitter = "@TomNomNom";
5 | json.github = "https://github.com/tomnomnom/";
6 | json.likes = [];
7 | json.likes[0] = "code";
8 | json.likes[1] = "cheese";
9 | json.likes[2] = "meat";
10 | json.name = "Tom";
11 |
--------------------------------------------------------------------------------
/testdata/two.jgron:
--------------------------------------------------------------------------------
1 | [[],{}]
2 | [["contact"],{}]
3 | [["contact","email"],"mail@tomnomnom.com"]
4 | [["contact","twitter"],"@TomNomNom"]
5 | [["github"],"https://github.com/tomnomnom/"]
6 | [["likes"],[]]
7 | [["likes",0],"code"]
8 | [["likes",1],"cheese"]
9 | [["likes",2],"meat"]
10 | [["name"],"Tom"]
11 |
--------------------------------------------------------------------------------
/testdata/two.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Tom",
3 | "github": "https://github.com/tomnomnom/",
4 | "likes": ["code", "cheese", "meat"],
5 | "contact": {
6 | "email": "mail@tomnomnom.com",
7 | "twitter": "@TomNomNom"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/token.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "unicode"
8 | )
9 |
10 | // A token is a chunk of text from a statement with a type
11 | type token struct {
12 | text string
13 | typ tokenTyp
14 | }
15 |
16 | // A tokenTyp identifies what kind of token something is
17 | type tokenTyp int
18 |
19 | const (
20 | // A bare word is a unquoted key; like 'foo' in json.foo = 1;
21 | typBare tokenTyp = iota
22 |
23 | // Numeric key; like '2' in json[2] = "foo";
24 | typNumericKey
25 |
26 | // A quoted key; like 'foo bar' in json["foo bar"] = 2;
27 | typQuotedKey
28 |
29 | // Punctuation types
30 | typDot // .
31 | typLBrace // [
32 | typRBrace // ]
33 | typEquals // =
34 | typSemi // ;
35 | typComma // ,
36 |
37 | // Value types
38 | typString // "foo"
39 | typNumber // 4
40 | typTrue // true
41 | typFalse // false
42 | typNull // null
43 | typEmptyArray // []
44 | typEmptyObject // {}
45 |
46 | // Ignored token
47 | typIgnored
48 |
49 | // Error token
50 | typError
51 | )
52 |
53 | // a sprintFn adds color to its input
54 | type sprintFn func(...interface{}) string
55 |
56 | // mapping of token types to the appropriate color sprintFn
57 | var sprintFns = map[tokenTyp]sprintFn{
58 | typBare: bareColor.SprintFunc(),
59 | typNumericKey: numColor.SprintFunc(),
60 | typQuotedKey: strColor.SprintFunc(),
61 | typLBrace: braceColor.SprintFunc(),
62 | typRBrace: braceColor.SprintFunc(),
63 | typString: strColor.SprintFunc(),
64 | typNumber: numColor.SprintFunc(),
65 | typTrue: boolColor.SprintFunc(),
66 | typFalse: boolColor.SprintFunc(),
67 | typNull: boolColor.SprintFunc(),
68 | typEmptyArray: braceColor.SprintFunc(),
69 | typEmptyObject: braceColor.SprintFunc(),
70 | }
71 |
72 | // isValue returns true if the token is a valid value type
73 | func (t token) isValue() bool {
74 | switch t.typ {
75 | case typString, typNumber, typTrue, typFalse, typNull, typEmptyArray, typEmptyObject:
76 | return true
77 | default:
78 | return false
79 | }
80 | }
81 |
82 | // isPunct returns true if the token is a punctuation type
83 | func (t token) isPunct() bool {
84 | switch t.typ {
85 | case typDot, typLBrace, typRBrace, typEquals, typSemi, typComma:
86 | return true
87 | default:
88 | return false
89 | }
90 | }
91 |
92 | // format returns the formatted version of the token text
93 | func (t token) format() string {
94 | if t.typ == typEquals {
95 | return " " + t.text + " "
96 | }
97 | return t.text
98 | }
99 |
100 | // formatColor returns the colored formatted version of the token text
101 | func (t token) formatColor() string {
102 | text := t.text
103 | if t.typ == typEquals {
104 | text = " " + text + " "
105 | }
106 | fn, ok := sprintFns[t.typ]
107 | if ok {
108 | return fn(text)
109 | }
110 | return text
111 |
112 | }
113 |
114 | // valueTokenFromInterface takes any valid value and
115 | // returns a value token to represent it
116 | func valueTokenFromInterface(v interface{}) token {
117 | switch vv := v.(type) {
118 |
119 | case map[string]interface{}:
120 | return token{"{}", typEmptyObject}
121 | case []interface{}:
122 | return token{"[]", typEmptyArray}
123 | case json.Number:
124 | return token{vv.String(), typNumber}
125 | case string:
126 | return token{quoteString(vv), typString}
127 | case bool:
128 | if vv {
129 | return token{"true", typTrue}
130 | }
131 | return token{"false", typFalse}
132 | case nil:
133 | return token{"null", typNull}
134 | default:
135 | return token{"", typError}
136 | }
137 | }
138 |
139 | // quoteString takes a string and returns a quoted and
140 | // escaped string valid for use in gron output
141 | func quoteString(s string) string {
142 |
143 | out := &bytes.Buffer{}
144 | // bytes.Buffer never returns errors on these methods.
145 | // errors are explicitly ignored to keep the linter
146 | // happy. A price worth paying so that the linter
147 | // remains useful.
148 | _ = out.WriteByte('"')
149 |
150 | for _, r := range s {
151 | switch r {
152 | case '\\':
153 | _, _ = out.WriteString(`\\`)
154 | case '"':
155 | _, _ = out.WriteString(`\"`)
156 | case '\b':
157 | _, _ = out.WriteString(`\b`)
158 | case '\f':
159 | _, _ = out.WriteString(`\f`)
160 | case '\n':
161 | _, _ = out.WriteString(`\n`)
162 | case '\r':
163 | _, _ = out.WriteString(`\r`)
164 | case '\t':
165 | _, _ = out.WriteString(`\t`)
166 | // \u2028 and \u2029 are separator runes that are not valid
167 | // in javascript strings so they must be escaped.
168 | // See http://timelessrepo.com/json-isnt-a-javascript-subset
169 | case '\u2028':
170 | _, _ = out.WriteString(`\u2028`)
171 | case '\u2029':
172 | _, _ = out.WriteString(`\u2029`)
173 | default:
174 | // Any other control runes must be escaped
175 | if unicode.IsControl(r) {
176 | _, _ = fmt.Fprintf(out, `\u%04X`, r)
177 | } else {
178 | // Unescaped rune
179 | _, _ = out.WriteRune(r)
180 | }
181 | }
182 | }
183 |
184 | _ = out.WriteByte('"')
185 | return out.String()
186 |
187 | }
188 |
--------------------------------------------------------------------------------
/token_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 | )
7 |
8 | var cases = []struct {
9 | in interface{}
10 | want token
11 | }{
12 | {make(map[string]interface{}), token{"{}", typEmptyObject}},
13 | {make([]interface{}, 0), token{"[]", typEmptyArray}},
14 | {json.Number("1.2"), token{"1.2", typNumber}},
15 | {"foo", token{`"foo"`, typString}},
16 | {"<3", token{`"<3"`, typString}},
17 | {"&", token{`"&"`, typString}},
18 | {"\b", token{`"\b"`, typString}},
19 | {"\f", token{`"\f"`, typString}},
20 | {"\n", token{`"\n"`, typString}},
21 | {"\r", token{`"\r"`, typString}},
22 | {"\t", token{`"\t"`, typString}},
23 | {"wat \u001e", token{`"wat \u001E"`, typString}},
24 | {"Hello, 世界", token{`"Hello, 世界"`, typString}},
25 | {true, token{"true", typTrue}},
26 | {false, token{"false", typFalse}},
27 | {nil, token{"null", typNull}},
28 | {struct{}{}, token{"", typError}},
29 | }
30 |
31 | func TestValueTokenFromInterface(t *testing.T) {
32 |
33 | for _, c := range cases {
34 | have := valueTokenFromInterface(c.in)
35 |
36 | if have != c.want {
37 | t.Logf("input: %#v", have)
38 | t.Logf("have: %#v", have)
39 | t.Logf("want: %#v", c.want)
40 | t.Errorf("have != want")
41 | }
42 | }
43 | }
44 |
45 | func BenchmarkValueTokenFromInterface(b *testing.B) {
46 |
47 | for i := 0; i < b.N; i++ {
48 | for _, c := range cases {
49 | _ = valueTokenFromInterface(c.in)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ungron.go:
--------------------------------------------------------------------------------
1 | // Ungronning is the reverse of gronning: turn statements
2 | // back into JSON. The expected input grammar is:
3 | //
4 | // Input ::= '--'* Statement (Statement | '--')*
5 | // Statement ::= Path Space* "=" Space* Value ";" "\n"
6 | // Path ::= (BareWord) ("." BareWord | ("[" Key "]"))*
7 | // Value ::= String | Number | "true" | "false" | "null" | "[]" | "{}"
8 | // BareWord ::= (UnicodeLu | UnicodeLl | UnicodeLm | UnicodeLo | UnicodeNl | '$' | '_') (UnicodeLu | UnicodeLl | UnicodeLm | UnicodeLo | UnicodeNl | UnicodeMn | UnicodeMc | UnicodeNd | UnicodePc | '$' | '_')*
9 | // Key ::= [0-9]+ | String
10 | // String ::= '"' (UnescapedRune | ("\" (["\/bfnrt] | ('u' Hex))))* '"'
11 | // UnescapedRune ::= [^#x0-#x1f"\]
12 |
13 | package main
14 |
15 | import (
16 | "encoding/json"
17 | "fmt"
18 | "reflect"
19 | "strconv"
20 | "strings"
21 | "unicode"
22 | "unicode/utf8"
23 |
24 | "github.com/pkg/errors"
25 | )
26 |
27 | // errRecoverable is an error type to represent errors that
28 | // can be recovered from; e.g. an empty line in the input
29 | type errRecoverable struct {
30 | msg string
31 | }
32 |
33 | func (e errRecoverable) Error() string {
34 | return e.msg
35 | }
36 |
37 | // A lexer holds the state for lexing statements
38 | type lexer struct {
39 | text string // The raw input text
40 | pos int // The current byte offset in the text
41 | width int // The width of the current rune in bytes
42 | cur rune // The rune at the current position
43 | prev rune // The rune at the previous position
44 | tokens []token // The tokens that have been emitted
45 | tokenStart int // The starting position of the current token
46 | }
47 |
48 | // newLexer returns a new lexer for the provided input string
49 | func newLexer(text string) *lexer {
50 | return &lexer{
51 | text: text,
52 | pos: 0,
53 | tokenStart: 0,
54 | tokens: make([]token, 0),
55 | }
56 | }
57 |
58 | // lex runs the lexer and returns the lexed statement
59 | func (l *lexer) lex() statement {
60 |
61 | for lexfn := lexStatement; lexfn != nil; {
62 | lexfn = lexfn(l)
63 | }
64 | return l.tokens
65 | }
66 |
67 | // next gets the next rune in the input and updates the lexer state
68 | func (l *lexer) next() rune {
69 | r, w := utf8.DecodeRuneInString(l.text[l.pos:])
70 |
71 | l.pos += w
72 | l.width = w
73 |
74 | l.prev = l.cur
75 | l.cur = r
76 |
77 | return r
78 | }
79 |
80 | // backup moves the lexer back one rune
81 | // can only be used once per call of next()
82 | func (l *lexer) backup() {
83 | l.pos -= l.width
84 | }
85 |
86 | // peek returns the next rune in the input
87 | // without moving the internal pointer
88 | func (l *lexer) peek() rune {
89 | r := l.next()
90 | l.backup()
91 | return r
92 | }
93 |
94 | // ignore skips the current token
95 | func (l *lexer) ignore() {
96 | l.tokenStart = l.pos
97 | }
98 |
99 | // emit adds the current token to the token slice and
100 | // moves the tokenStart pointer to the current position
101 | func (l *lexer) emit(typ tokenTyp) {
102 | t := token{
103 | text: l.text[l.tokenStart:l.pos],
104 | typ: typ,
105 | }
106 | l.tokenStart = l.pos
107 |
108 | l.tokens = append(l.tokens, t)
109 | }
110 |
111 | // accept moves the pointer if the next rune is in
112 | // the set of valid runes
113 | func (l *lexer) accept(valid string) bool {
114 | if strings.ContainsRune(valid, l.next()) {
115 | return true
116 | }
117 | l.backup()
118 | return false
119 | }
120 |
121 | // acceptRun continually accepts runes from the
122 | // set of valid runes
123 | func (l *lexer) acceptRun(valid string) {
124 | for strings.ContainsRune(valid, l.next()) {
125 | }
126 | l.backup()
127 | }
128 |
129 | // a runeCheck is a function that determines if a rune is valid
130 | // or not so that we can do complex checks against runes
131 | type runeCheck func(rune) bool
132 |
133 | // acceptFunc accepts a rune if the provided runeCheck
134 | // function returns true
135 | func (l *lexer) acceptFunc(fn runeCheck) bool {
136 | if fn(l.next()) {
137 | return true
138 | }
139 | l.backup()
140 | return false
141 | }
142 |
143 | // acceptRunFunc continually accepts runes for as long
144 | // as the runeCheck function returns true
145 | func (l *lexer) acceptRunFunc(fn runeCheck) {
146 | for fn(l.next()) {
147 | }
148 | l.backup()
149 | }
150 |
151 | // acceptUntil accepts runes until it hits a delimiter
152 | // rune contained in the provided string
153 | func (l *lexer) acceptUntil(delims string) {
154 | for !strings.ContainsRune(delims, l.next()) {
155 | if l.cur == utf8.RuneError {
156 | return
157 | }
158 | }
159 | l.backup()
160 | }
161 |
162 | // acceptUntilUnescaped accepts runes until it hits a delimiter
163 | // rune contained in the provided string, unless that rune was
164 | // escaped with a backslash
165 | func (l *lexer) acceptUntilUnescaped(delims string) {
166 |
167 | // Read until we hit an unescaped rune or the end of the input
168 | inEscape := false
169 | for {
170 | r := l.next()
171 | if r == '\\' && !inEscape {
172 | inEscape = true
173 | continue
174 | }
175 | if strings.ContainsRune(delims, r) && !inEscape {
176 | l.backup()
177 | return
178 | }
179 | if l.cur == utf8.RuneError {
180 | return
181 | }
182 | inEscape = false
183 | }
184 | }
185 |
186 | // a lexFn accepts a lexer, performs some action on it and
187 | // then returns an appropriate lexFn for the next stage
188 | type lexFn func(*lexer) lexFn
189 |
190 | // lexStatement is the highest level lexFn. Its only job
191 | // is to determine which more specific lexFn to use
192 | func lexStatement(l *lexer) lexFn {
193 | r := l.peek()
194 |
195 | switch {
196 | case r == '.' || validFirstRune(r):
197 | return lexBareWord
198 | case r == '[':
199 | return lexBraces
200 | case r == ' ', r == '=':
201 | return lexValue
202 | case r == '-':
203 | // grep -A etc can add '--' lines to output
204 | // we'll save the text but not actually do
205 | // anything with them
206 | return lexIgnore
207 | case r == utf8.RuneError:
208 | return nil
209 | default:
210 | l.emit(typError)
211 | return nil
212 | }
213 |
214 | }
215 |
216 | // lexBareWord lexes for bare identifiers.
217 | // E.g: the 'foo' in 'foo.bar' or 'foo[0]' is a bare identifier
218 | func lexBareWord(l *lexer) lexFn {
219 | if l.accept(".") {
220 | l.emit(typDot)
221 | }
222 |
223 | if !l.acceptFunc(validFirstRune) {
224 | l.emit(typError)
225 | return nil
226 | }
227 | l.acceptRunFunc(validSecondaryRune)
228 | l.emit(typBare)
229 |
230 | return lexStatement
231 | }
232 |
233 | // lexBraces lexes keys contained within square braces
234 | func lexBraces(l *lexer) lexFn {
235 | l.accept("[")
236 | l.emit(typLBrace)
237 |
238 | switch {
239 | case unicode.IsNumber(l.peek()):
240 | return lexNumericKey
241 | case l.peek() == '"':
242 | return lexQuotedKey
243 | default:
244 | l.emit(typError)
245 | return nil
246 | }
247 | }
248 |
249 | // lexNumericKey lexes numeric keys between square braces
250 | func lexNumericKey(l *lexer) lexFn {
251 | l.accept("[")
252 | l.ignore()
253 |
254 | l.acceptRunFunc(unicode.IsNumber)
255 | l.emit(typNumericKey)
256 |
257 | if l.accept("]") {
258 | l.emit(typRBrace)
259 | } else {
260 | l.emit(typError)
261 | return nil
262 | }
263 | l.ignore()
264 | return lexStatement
265 | }
266 |
267 | // lexQuotedKey lexes quoted keys between square braces
268 | func lexQuotedKey(l *lexer) lexFn {
269 | l.accept("[")
270 | l.ignore()
271 |
272 | l.accept(`"`)
273 |
274 | l.acceptUntilUnescaped(`"`)
275 | l.accept(`"`)
276 | l.emit(typQuotedKey)
277 |
278 | if l.accept("]") {
279 | l.emit(typRBrace)
280 | } else {
281 | l.emit(typError)
282 | return nil
283 | }
284 | l.ignore()
285 | return lexStatement
286 | }
287 |
288 | // lexValue lexes a value at the end of a statement
289 | func lexValue(l *lexer) lexFn {
290 | l.acceptRun(" ")
291 | l.ignore()
292 |
293 | if l.accept("=") {
294 | l.emit(typEquals)
295 | } else {
296 | return nil
297 | }
298 | l.acceptRun(" ")
299 | l.ignore()
300 |
301 | switch {
302 |
303 | case l.accept(`"`):
304 | l.acceptUntilUnescaped(`"`)
305 | l.accept(`"`)
306 | l.emit(typString)
307 |
308 | case l.accept("t"):
309 | l.acceptRun("rue")
310 | l.emit(typTrue)
311 |
312 | case l.accept("f"):
313 | l.acceptRun("alse")
314 | l.emit(typFalse)
315 |
316 | case l.accept("n"):
317 | l.acceptRun("ul")
318 | l.emit(typNull)
319 |
320 | case l.accept("["):
321 | l.accept("]")
322 | l.emit(typEmptyArray)
323 |
324 | case l.accept("{"):
325 | l.accept("}")
326 | l.emit(typEmptyObject)
327 |
328 | default:
329 | // Assume number
330 | l.acceptUntil(";")
331 | l.emit(typNumber)
332 | }
333 |
334 | l.acceptRun(" ")
335 | l.ignore()
336 |
337 | if l.accept(";") {
338 | l.emit(typSemi)
339 | }
340 |
341 | // The value should always be the last thing
342 | // in the statement
343 | return nil
344 | }
345 |
346 | // lexIgnore accepts runes until the end of the input
347 | // and emits them as a typIgnored token
348 | func lexIgnore(l *lexer) lexFn {
349 | l.acceptRunFunc(func(r rune) bool {
350 | return r != utf8.RuneError
351 | })
352 | l.emit(typIgnored)
353 | return nil
354 | }
355 |
356 | // ungronTokens turns a slice of tokens into an actual datastructure
357 | func ungronTokens(ts []token) (interface{}, error) {
358 | if len(ts) == 0 {
359 | return nil, errRecoverable{"empty input"}
360 | }
361 |
362 | if ts[0].typ == typIgnored {
363 | return nil, errRecoverable{"ignored token"}
364 | }
365 |
366 | if ts[len(ts)-1].typ == typError {
367 | return nil, errors.New("invalid statement")
368 | }
369 |
370 | // The last token should be typSemi so we need to check
371 | // the second to last token is a value rather than the
372 | // last one
373 | if len(ts) > 1 && !ts[len(ts)-2].isValue() {
374 | return nil, errors.New("statement has no value")
375 | }
376 |
377 | t := ts[0]
378 | switch {
379 | case t.isPunct():
380 | // Skip the token
381 | val, err := ungronTokens(ts[1:])
382 | if err != nil {
383 | return nil, err
384 | }
385 | return val, nil
386 |
387 | case t.isValue():
388 | var val interface{}
389 | d := json.NewDecoder(strings.NewReader(t.text))
390 | d.UseNumber()
391 | err := d.Decode(&val)
392 | if err != nil {
393 | return nil, fmt.Errorf("invalid value `%s`", t.text)
394 | }
395 | return val, nil
396 |
397 | case t.typ == typBare:
398 | val, err := ungronTokens(ts[1:])
399 | if err != nil {
400 | return nil, err
401 | }
402 | out := make(map[string]interface{})
403 | out[t.text] = val
404 | return out, nil
405 |
406 | case t.typ == typQuotedKey:
407 | val, err := ungronTokens(ts[1:])
408 | if err != nil {
409 | return nil, err
410 | }
411 | key := ""
412 | err = json.Unmarshal([]byte(t.text), &key)
413 | if err != nil {
414 | return nil, fmt.Errorf("invalid quoted key `%s`", t.text)
415 | }
416 |
417 | out := make(map[string]interface{})
418 | out[key] = val
419 | return out, nil
420 |
421 | case t.typ == typNumericKey:
422 | key, err := strconv.Atoi(t.text)
423 | if err != nil {
424 | return nil, fmt.Errorf("invalid integer key `%s`", t.text)
425 | }
426 |
427 | val, err := ungronTokens(ts[1:])
428 | if err != nil {
429 | return nil, err
430 | }
431 |
432 | // There needs to be at least key + 1 space in the array
433 | out := make([]interface{}, key+1)
434 | out[key] = val
435 | return out, nil
436 |
437 | default:
438 | return nil, fmt.Errorf("unexpected token `%s`", t.text)
439 | }
440 | }
441 |
442 | // recursiveMerge merges maps and slices, or returns b for scalars
443 | func recursiveMerge(a, b interface{}) (interface{}, error) {
444 | switch a.(type) {
445 |
446 | case map[string]interface{}:
447 | bMap, ok := b.(map[string]interface{})
448 | if !ok {
449 | return nil, fmt.Errorf("cannot merge object with non-object")
450 | }
451 | return recursiveMapMerge(a.(map[string]interface{}), bMap)
452 |
453 | case []interface{}:
454 | bSlice, ok := b.([]interface{})
455 | if !ok {
456 | return nil, fmt.Errorf("cannot merge array with non-array")
457 | }
458 | return recursiveSliceMerge(a.([]interface{}), bSlice)
459 |
460 | case string, int, float64, bool, nil, json.Number:
461 | // Can't merge them, second one wins
462 | return b, nil
463 |
464 | default:
465 | return nil, fmt.Errorf("unexpected data type for merge: `%s`", reflect.TypeOf(a))
466 | }
467 | }
468 |
469 | // recursiveMapMerge recursively merges map[string]interface{} values
470 | func recursiveMapMerge(a, b map[string]interface{}) (map[string]interface{}, error) {
471 | // Merge keys from b into a
472 | for k, v := range b {
473 | _, exists := a[k]
474 | if !exists {
475 | // Doesn't exist in a, just add it in
476 | a[k] = v
477 | } else {
478 | // Does exist, merge the values
479 | merged, err := recursiveMerge(a[k], b[k])
480 | if err != nil {
481 | return nil, err
482 | }
483 |
484 | a[k] = merged
485 | }
486 | }
487 | return a, nil
488 | }
489 |
490 | // recursiveSliceMerge recursively merged []interface{} values
491 | func recursiveSliceMerge(a, b []interface{}) ([]interface{}, error) {
492 | // We need a new slice with the capacity of whichever
493 | // slive is biggest
494 | outLen := len(a)
495 | if len(b) > outLen {
496 | outLen = len(b)
497 | }
498 | out := make([]interface{}, outLen)
499 |
500 | // Copy the values from 'a' into the output slice
501 | copy(out, a)
502 |
503 | // Add the values from 'b'; merging existing keys
504 | for k, v := range b {
505 | if out[k] == nil {
506 | out[k] = v
507 | } else if v != nil {
508 | merged, err := recursiveMerge(out[k], b[k])
509 | if err != nil {
510 | return nil, err
511 | }
512 | out[k] = merged
513 | }
514 | }
515 | return out, nil
516 | }
517 |
--------------------------------------------------------------------------------
/ungron_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestLex(t *testing.T) {
9 | cases := []struct {
10 | in string
11 | want []token
12 | }{
13 | {`json.foo = 1;`, []token{
14 | {`json`, typBare},
15 | {`.`, typDot},
16 | {`foo`, typBare},
17 | {`=`, typEquals},
18 | {`1`, typNumber},
19 | {`;`, typSemi},
20 | }},
21 |
22 | {`json.foo = "bar";`, []token{
23 | {`json`, typBare},
24 | {`.`, typDot},
25 | {`foo`, typBare},
26 | {`=`, typEquals},
27 | {`"bar"`, typString},
28 | {`;`, typSemi},
29 | }},
30 |
31 | {`json.foo = "ba;r";`, []token{
32 | {`json`, typBare},
33 | {`.`, typDot},
34 | {`foo`, typBare},
35 | {`=`, typEquals},
36 | {`"ba;r"`, typString},
37 | {`;`, typSemi},
38 | }},
39 |
40 | {`json.foo = "ba\"r ;";`, []token{
41 | {`json`, typBare},
42 | {`.`, typDot},
43 | {`foo`, typBare},
44 | {`=`, typEquals},
45 | {`"ba\"r ;"`, typString},
46 | {`;`, typSemi},
47 | }},
48 |
49 | {`json = "\\";`, []token{
50 | {`json`, typBare},
51 | {`=`, typEquals},
52 | {`"\\"`, typString},
53 | {`;`, typSemi},
54 | }},
55 |
56 | {`json = "\\\\";`, []token{
57 | {`json`, typBare},
58 | {`=`, typEquals},
59 | {`"\\\\"`, typString},
60 | {`;`, typSemi},
61 | }},
62 |
63 | {`json = "f\oo\\";`, []token{
64 | {`json`, typBare},
65 | {`=`, typEquals},
66 | {`"f\oo\\"`, typString},
67 | {`;`, typSemi},
68 | }},
69 |
70 | {`json.value = "\u003c ;";`, []token{
71 | {`json`, typBare},
72 | {`.`, typDot},
73 | {`value`, typBare},
74 | {`=`, typEquals},
75 | {`"\u003c ;"`, typString},
76 | {`;`, typSemi},
77 | }},
78 |
79 | {`json[0] = "bar";`, []token{
80 | {`json`, typBare},
81 | {`[`, typLBrace},
82 | {`0`, typNumericKey},
83 | {`]`, typRBrace},
84 | {`=`, typEquals},
85 | {`"bar"`, typString},
86 | {`;`, typSemi},
87 | }},
88 |
89 | {`json["foo"] = "bar";`, []token{
90 | {`json`, typBare},
91 | {`[`, typLBrace},
92 | {`"foo"`, typQuotedKey},
93 | {`]`, typRBrace},
94 | {`=`, typEquals},
95 | {`"bar"`, typString},
96 | {`;`, typSemi},
97 | }},
98 |
99 | {`json.foo["bar"][0] = "bar";`, []token{
100 | {`json`, typBare},
101 | {`.`, typDot},
102 | {`foo`, typBare},
103 | {`[`, typLBrace},
104 | {`"bar"`, typQuotedKey},
105 | {`]`, typRBrace},
106 | {`[`, typLBrace},
107 | {`0`, typNumericKey},
108 | {`]`, typRBrace},
109 | {`=`, typEquals},
110 | {`"bar"`, typString},
111 | {`;`, typSemi},
112 | }},
113 |
114 | {`not an identifier at all`, []token{
115 | {`not`, typBare},
116 | }},
117 |
118 | {`alsonotanidentifier`, []token{
119 | {`alsonotanidentifier`, typBare},
120 | }},
121 |
122 | {`wat!`, []token{
123 | {`wat`, typBare},
124 | {``, typError},
125 | }},
126 |
127 | {`json[ = 1;`, []token{
128 | {`json`, typBare},
129 | {`[`, typLBrace},
130 | {``, typError},
131 | }},
132 |
133 | {`json.[2] = 1;`, []token{
134 | {`json`, typBare},
135 | {`.`, typDot},
136 | {``, typError},
137 | }},
138 |
139 | {`json[1 = 1;`, []token{
140 | {`json`, typBare},
141 | {`[`, typLBrace},
142 | {`1`, typNumericKey},
143 | {``, typError},
144 | }},
145 |
146 | {`json["foo] = 1;`, []token{
147 | {`json`, typBare},
148 | {`[`, typLBrace},
149 | {`"foo] = 1;`, typQuotedKey},
150 | {``, typError},
151 | }},
152 |
153 | {`--`, []token{
154 | {`--`, typIgnored},
155 | }},
156 |
157 | {`json = 1;`, []token{
158 | {`json`, typBare},
159 | {`=`, typEquals},
160 | {`1`, typNumber},
161 | {`;`, typSemi},
162 | }},
163 |
164 | {`json=1;`, []token{
165 | {`json`, typBare},
166 | {`=`, typEquals},
167 | {`1`, typNumber},
168 | {`;`, typSemi},
169 | }},
170 | }
171 |
172 | for _, c := range cases {
173 | l := newLexer(c.in)
174 | have := l.lex()
175 |
176 | if len(have) != len(c.want) {
177 | t.Logf("Input: %#v", c.in)
178 | t.Logf("Want: %#v", c.want)
179 | t.Logf("Have: %#v", have)
180 | t.Fatalf("want %d tokens, have %d", len(c.want), len(have))
181 | }
182 |
183 | for i := range have {
184 | if have[i] != c.want[i] {
185 | t.Logf("Input: %#v", c.in)
186 | t.Logf("Want: %#v", c.want)
187 | t.Logf("Have: %#v", have)
188 | t.Errorf("Want `%#v` in position %d, have `%#v`", c.want[i], i, have[i])
189 | }
190 | }
191 | }
192 | }
193 |
194 | func TestUngronTokensSimple(t *testing.T) {
195 | in := `json.contact["e-mail"][0] = "mail@tomnomnom.com";`
196 | want := map[string]interface{}{
197 | "json": map[string]interface{}{
198 | "contact": map[string]interface{}{
199 | "e-mail": []interface{}{
200 | "mail@tomnomnom.com",
201 | },
202 | },
203 | },
204 | }
205 |
206 | l := newLexer(in)
207 | tokens := l.lex()
208 | have, err := ungronTokens(tokens)
209 |
210 | if err != nil {
211 | t.Fatalf("failed to ungron statement: %s", err)
212 | }
213 |
214 | t.Logf("Have: %#v", have)
215 | t.Logf("Want: %#v", want)
216 |
217 | eq := reflect.DeepEqual(have, want)
218 | if !eq {
219 | t.Errorf("Have and want datastructures are unequal")
220 | }
221 | }
222 |
223 | func TestUngronTokensInvalid(t *testing.T) {
224 | cases := []struct {
225 | in []token
226 | }{
227 | {[]token{{``, typError}}}, // Error token
228 | {[]token{{`foo`, typString}}}, // Invalid value
229 | {[]token{{`"foo`, typQuotedKey}, {"1", typNumber}}}, // Invalid quoted key
230 | {[]token{{`foo`, typNumericKey}, {"1", typNumber}}}, // Invalid numeric key
231 | {[]token{{``, -255}, {"1", typNumber}}}, // Invalid token type
232 | }
233 |
234 | for _, c := range cases {
235 | _, err := ungronTokens(c.in)
236 | if err == nil {
237 | t.Errorf("want non-nil error for %#v; have nil", c.in)
238 | }
239 | }
240 | }
241 |
242 | func TestMerge(t *testing.T) {
243 | a := map[string]interface{}{
244 | "json": map[string]interface{}{
245 | "contact": map[string]interface{}{
246 | "e-mail": []interface{}{
247 | 0: "mail@tomnomnom.com",
248 | },
249 | },
250 | },
251 | }
252 |
253 | b := map[string]interface{}{
254 | "json": map[string]interface{}{
255 | "contact": map[string]interface{}{
256 | "e-mail": []interface{}{
257 | 1: "test@tomnomnom.com",
258 | 3: "foo@tomnomnom.com",
259 | },
260 | "twitter": "@TomNomNom",
261 | },
262 | },
263 | }
264 |
265 | want := map[string]interface{}{
266 | "json": map[string]interface{}{
267 | "contact": map[string]interface{}{
268 | "e-mail": []interface{}{
269 | 0: "mail@tomnomnom.com",
270 | 1: "test@tomnomnom.com",
271 | 3: "foo@tomnomnom.com",
272 | },
273 | "twitter": "@TomNomNom",
274 | },
275 | },
276 | }
277 |
278 | t.Logf("A: %#v", a)
279 | t.Logf("B: %#v", b)
280 | have, err := recursiveMerge(a, b)
281 | if err != nil {
282 | t.Fatalf("failed to merge datastructures: %s", err)
283 | }
284 |
285 | t.Logf("Have: %#v", have)
286 | t.Logf("Want: %#v", want)
287 | eq := reflect.DeepEqual(have, want)
288 | if !eq {
289 | t.Errorf("Have and want datastructures are unequal")
290 | }
291 |
292 | }
293 |
--------------------------------------------------------------------------------
/url.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "crypto/tls"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | neturl "net/url"
10 | "os"
11 | "regexp"
12 | "strings"
13 | "time"
14 | )
15 |
16 | func validURL(url string) bool {
17 | r := regexp.MustCompile("(?i)^http(?:s)?://")
18 | return r.MatchString(url)
19 | }
20 |
21 | func configureProxy(url string, proxy string, noProxy string) func(*http.Request) (*neturl.URL, error) {
22 | cURL, err := neturl.Parse(url)
23 | if err != nil {
24 | return nil
25 | }
26 |
27 | // Direct arguments are superior to environment variables.
28 | if proxy == undefinedProxy {
29 | proxy = os.Getenv(fmt.Sprintf("%s_proxy", cURL.Scheme))
30 | }
31 | if noProxy == undefinedProxy {
32 | noProxy = os.Getenv("no_proxy")
33 | }
34 |
35 | // Skip setting a proxy if no proxy has been set through env variable or
36 | // argument.
37 | if proxy == "" {
38 | return nil
39 | }
40 |
41 | // Test if any of the hosts mentioned in the noProxy variable or the
42 | // no_proxy env variable. Skip setting up the proxy if a match is found.
43 | noProxyHosts := strings.Split(noProxy, ",")
44 | if len(noProxyHosts) > 0 {
45 | for _, noProxyHost := range noProxyHosts {
46 | if len(noProxyHost) == 0 {
47 | continue
48 | }
49 | // Test for direct matches of the hostname.
50 | if cURL.Host == noProxyHost {
51 | return nil
52 | }
53 | // Match through wildcard-like pattern, e.g. ".foobar.com" should
54 | // match all subdomains of foobar.com.
55 | if strings.HasPrefix(noProxyHost, ".") && strings.HasSuffix(cURL.Host, noProxyHost) {
56 | return nil
57 | }
58 | }
59 | }
60 |
61 | proxyURL, err := neturl.Parse(proxy)
62 | if err != nil {
63 | return nil
64 | }
65 |
66 | return http.ProxyURL(proxyURL)
67 | }
68 |
69 | func getURL(url string, insecure bool, proxyURL string, noProxy string) (io.Reader, error) {
70 | tr := &http.Transport{
71 | TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
72 | }
73 | // Set proxy if defined.
74 | proxy := configureProxy(url, proxyURL, noProxy)
75 | if proxy != nil {
76 | tr.Proxy = proxy
77 | }
78 | client := http.Client{
79 | Transport: tr,
80 | Timeout: 20 * time.Second,
81 | }
82 |
83 | req, err := http.NewRequest("GET", url, nil)
84 | if err != nil {
85 | return nil, err
86 | }
87 | req.Header.Set("User-Agent", fmt.Sprintf("gron/%s", gronVersion))
88 | req.Header.Set("Accept", "application/json")
89 |
90 | resp, err := client.Do(req)
91 |
92 | if err != nil {
93 | return nil, err
94 | }
95 |
96 | return bufio.NewReader(resp.Body), err
97 | }
98 |
--------------------------------------------------------------------------------
/url_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "testing"
6 | )
7 |
8 | func TestValidURL(t *testing.T) {
9 | tests := []struct {
10 | url string
11 | want bool
12 | }{
13 | {"http://test.com", true},
14 | {"https://test.com", true},
15 | {"HttPs://test.com", true},
16 | {"/test/test.com", false},
17 | {"", false},
18 | }
19 |
20 | for _, test := range tests {
21 | have := validURL(test.url)
22 | if have != test.want {
23 | t.Errorf("Want %t for validURL(%s); have %t", test.want, test.url, have)
24 | }
25 | }
26 | }
27 |
28 | func TestConfigureProxyHttp(t *testing.T) {
29 | tests := []struct {
30 | url string
31 | httpProxy string
32 | envHttpProxy string
33 | noProxy string
34 | envNoProxy string
35 | hasProxy bool
36 | }{
37 | // http proxy via env variables
38 | {"http://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", true},
39 | {"https://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", false},
40 | {"schema://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", false},
41 |
42 | // http proxy with env variables, overwritten by argument
43 | {"http://test2.com", "", "http://localhost:1234", undefinedProxy, "", false},
44 | {"https://test2.com", "", "http://localhost:1234", undefinedProxy, "", false},
45 | {"schema://test2.com", "", "http://localhost:1234", undefinedProxy, "", false},
46 |
47 | // http proxy with env variables, domain excluded by no_proxy
48 | {"http://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false},
49 | {"http://foobar3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false},
50 | {"http://test.foobar3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, ".foobar3.com", false},
51 | {"https://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false},
52 | {"schema://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false},
53 |
54 | // http proxy with env variables, domain excluded by no_proxy, overwritten by argument
55 | {"http://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", true},
56 | {"http://foobar4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", true},
57 | {"http://test.foobar4.com", undefinedProxy, "http://localhost:1234", "", ".foobar4.com", true},
58 | {"https://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false},
59 | {"schema://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false},
60 | }
61 |
62 | for _, test := range tests {
63 | os.Setenv("http_proxy", test.envHttpProxy)
64 | os.Setenv("no_proxy", test.envNoProxy)
65 | proxy := configureProxy(test.url, test.httpProxy, test.noProxy)
66 | hasProxy := proxy != nil
67 | if hasProxy != test.hasProxy {
68 | t.Errorf("Want %t for configureProxy; have %t; %v", test.hasProxy, hasProxy, test)
69 | }
70 | os.Unsetenv("http_proxy")
71 | os.Unsetenv("no_proxy")
72 | }
73 | }
74 |
75 | func TestConfigureProxyHttps(t *testing.T) {
76 | tests := []struct {
77 | url string
78 | httpsProxy string
79 | envHttpsProxy string
80 | noProxy string
81 | envNoProxy string
82 | hasProxy bool
83 | }{
84 | // https proxy via env variables
85 | {"http://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", false},
86 | {"https://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", true},
87 | {"schema://test1.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "", false},
88 |
89 | // https proxy with env variables, overwritten by argument
90 | {"http://test2.com", "", "http://localhost:1234", undefinedProxy, "", false},
91 | {"https://test2.com", "", "http://localhost:1234", undefinedProxy, "", false},
92 | {"schema://test2.com", "", "http://localhost:1234", undefinedProxy, "", false},
93 |
94 | // https proxy with env variables, domain excluded by no_proxy
95 | {"http://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false},
96 | {"http://foobar3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false},
97 | {"http://test.foobar3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, ".foobar3.com", false},
98 | {"https://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false},
99 | {"schema://test3.com", undefinedProxy, "http://localhost:1234", undefinedProxy, "test3.com,foobar3.com", false},
100 |
101 | // https proxy with env variables, domain excluded by no_proxy, overwritten by argument
102 | {"http://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false},
103 | {"http://foobar4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false},
104 | {"http://test.foobar4.com", undefinedProxy, "http://localhost:1234", "", ".foobar4.com", false},
105 | {"https://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", true},
106 | {"schema://test4.com", undefinedProxy, "http://localhost:1234", "", "test4.com,foobar4.com", false},
107 | }
108 |
109 | for _, test := range tests {
110 | os.Setenv("https_proxy", test.envHttpsProxy)
111 | os.Setenv("no_proxy", test.envNoProxy)
112 | proxy := configureProxy(test.url, test.httpsProxy, test.noProxy)
113 | hasProxy := proxy != nil
114 | if hasProxy != test.hasProxy {
115 | t.Errorf("Want %t for configureProxy; have %t; %v", test.hasProxy, hasProxy, test)
116 | }
117 | os.Unsetenv("https_proxy")
118 | os.Unsetenv("no_proxy")
119 | }
120 | }
121 |
--------------------------------------------------------------------------------