├── .github
├── dependabot.yml
└── workflows
│ ├── go.yml
│ └── golangci-lint.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── README.md
├── SECURITY.md
├── SEGMENTIO_README.md
├── ascii.go
├── ascii_test.go
├── benchmark_test.go
├── cmd
└── jc
│ └── main.go
├── codec.go
├── decode.go
├── encode.go
├── example_test.go
├── go.mod
├── go.sum
├── golang_bench_test.go
├── golang_decode_test.go
├── golang_encode_test.go
├── golang_example_marshaling_test.go
├── golang_example_test.go
├── golang_number_test.go
├── golang_scanner_test.go
├── golang_shim_test.go
├── golang_tagkey_test.go
├── helper
└── fatihcolor
│ └── fatihcolor.go
├── json.go
├── json_test.go
├── jsoncolor.go
├── jsoncolor_internal_test.go
├── jsoncolor_test.go
├── parse.go
├── parse_test.go
├── reflect.go
├── reflect_optimize.go
├── splash.png
├── terminal.go
├── terminal_windows.go
├── testdata
├── code.json.gz
├── example.json
├── msgs.json.gz
├── sakila_actor.json
└── sakila_payment.json
├── token.go
└── token_test.go
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "gomod" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - '**.md'
7 | pull_request:
8 | paths-ignore:
9 | - '**.md'
10 |
11 | jobs:
12 | build:
13 | strategy:
14 | matrix:
15 | os: [ macos-latest, ubuntu-latest, windows-latest]
16 |
17 | runs-on: ${{ matrix.os }}
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 | - uses: actions/setup-go@v4
22 | with:
23 | go-version: ">= 1.16"
24 |
25 | - name: Get dependencies
26 | run: |
27 | go get -v -t ./...
28 |
29 | - name: Build
30 | run: go build -v .
31 |
32 | - name: Test
33 | run: go test -v .
34 |
--------------------------------------------------------------------------------
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 | on:
3 | push:
4 | paths-ignore:
5 | - '**.md'
6 | pull_request:
7 | paths-ignore:
8 | - '**.md'
9 |
10 | permissions:
11 | contents: read
12 | # Optional: allow read access to pull request. Use with `only-new-issues` option.
13 | pull-requests: read
14 |
15 | jobs:
16 | golangci:
17 | name: lint
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v3
21 | - uses: actions/setup-go@v4
22 | with:
23 | go-version: ">= 1.16"
24 | cache: false
25 | - name: golangci-lint
26 | uses: golangci/golangci-lint-action@v3
27 | with:
28 | # Require: The version of golangci-lint to use.
29 | # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version.
30 | # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit.
31 | version: v1.54
32 |
33 | # Optional: working directory, useful for monorepos
34 | # working-directory: somedir
35 |
36 | # Optional: golangci-lint command line arguments.
37 | #
38 | # Note: By default, the `.golangci.yml` file should be at the root of the repository.
39 | # The location of the configuration file can be changed by using `--config=`
40 | # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0
41 |
42 | # We're explicitly setting the exit code to 0 because we don't want the CI to fail
43 | # if there are linting errors right now (because there's a bunch of linting errors!)
44 | # Ultimately we want to set the exit code to 1.
45 | args: --issues-exit-code=1 --out-format=colored-line-number
46 |
47 | # Optional: show only new issues if it's a pull request. The default value is `false`.
48 | # only-new-issues: true
49 |
50 | # Optional: if set to true, then all caching functionality will be completely disabled,
51 | # takes precedence over all other caching options.
52 | # skip-cache: true
53 |
54 | # Optional: if set to true, then the action won't cache or restore ~/go/pkg.
55 | # skip-pkg-cache: true
56 |
57 | # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build.
58 | # skip-build-cache: true
59 |
60 | # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'.
61 | # install-mode: "goinstall"
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | *.iml
18 | .idea
19 | TODO.md
20 | **/.DS_Store
21 | /scratch/
22 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | # This code is licensed under the terms of the MIT license.
2 |
3 | ## Golden config for golangci-lint v1.54
4 | #
5 | # This is the best config for golangci-lint based on my experience and opinion.
6 | # It is very strict, but not extremely strict.
7 | # Feel free to adopt and change it for your needs.
8 | #
9 | # @neilotoole: ^^ Well, it's less strict now!
10 | # Based on: https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
11 |
12 | run:
13 | # Timeout for analysis, e.g. 30s, 5m.
14 | # Default: 1m
15 | timeout: 5m
16 |
17 | tests: false
18 |
19 | skip-dirs:
20 | - scratch
21 |
22 |
23 |
24 |
25 |
26 | output:
27 | sort-results: true
28 |
29 | # This file contains only configs which differ from defaults.
30 | # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
31 | linters-settings:
32 | cyclop:
33 | # The maximal code complexity to report.
34 | # Default: 10
35 | max-complexity: 50
36 | # The maximal average package complexity.
37 | # If it's higher than 0.0 (float) the check is enabled
38 | # Default: 0.0
39 | package-average: 10.0
40 |
41 | errcheck:
42 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
43 | # Such cases aren't reported by default.
44 | # Default: false
45 | check-type-assertions: true
46 |
47 | exhaustive:
48 | # Program elements to check for exhaustiveness.
49 | # Default: [ switch ]
50 | check:
51 | - switch
52 | - map
53 |
54 | funlen:
55 | # Checks the number of lines in a function.
56 | # If lower than 0, disable the check.
57 | # Default: 60
58 | lines: 150
59 | # Checks the number of statements in a function.
60 | # If lower than 0, disable the check.
61 | # Default: 40
62 | statements: 100
63 |
64 | gocognit:
65 | # Minimal code complexity to report
66 | # Default: 30 (but we recommend 10-20)
67 | min-complexity: 50
68 |
69 | gocritic:
70 | # Settings passed to gocritic.
71 | # The settings key is the name of a supported gocritic checker.
72 | # The list of supported checkers can be find in https://go-critic.github.io/overview.
73 | settings:
74 | captLocal:
75 | # Whether to restrict checker to params only.
76 | # Default: true
77 | paramsOnly: false
78 | underef:
79 | # Whether to skip (*x).method() calls where x is a pointer receiver.
80 | # Default: true
81 | skipRecvDeref: false
82 |
83 | gocyclo:
84 | # Minimal code complexity to report.
85 | # Default: 30 (but we recommend 10-20)
86 | min-complexity: 50
87 |
88 | gofumpt:
89 | # Module path which contains the source code being formatted.
90 | # Default: ""
91 | module-path: github.com/neilotoole/jsoncolor
92 | # Choose whether to use the extra rules.
93 | # Default: false
94 | extra-rules: true
95 |
96 | gomnd:
97 | # List of function patterns to exclude from analysis.
98 | # Values always ignored: `time.Date`,
99 | # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
100 | # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
101 | # Default: []
102 | ignored-functions:
103 | - make
104 | - os.Chmod
105 | - os.Mkdir
106 | - os.MkdirAll
107 | - os.OpenFile
108 | - os.WriteFile
109 | - prometheus.ExponentialBuckets
110 | - prometheus.ExponentialBucketsRange
111 | - prometheus.LinearBuckets
112 | ignored-numbers:
113 | - "2"
114 | - "3"
115 |
116 | gomodguard:
117 | blocked:
118 | # List of blocked modules.
119 | # Default: []
120 | modules:
121 | - github.com/golang/protobuf:
122 | recommendations:
123 | - google.golang.org/protobuf
124 | reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
125 | - github.com/satori/go.uuid:
126 | recommendations:
127 | - github.com/google/uuid
128 | reason: "satori's package is not maintained"
129 | - github.com/gofrs/uuid:
130 | recommendations:
131 | - github.com/google/uuid
132 | reason: "gofrs' package is not go module"
133 |
134 | govet:
135 | # Enable all analyzers.
136 | # Default: false
137 | enable-all: true
138 | # Disable analyzers by name.
139 | # Run `go tool vet help` to see all analyzers.
140 | # Default: []
141 | disable:
142 | - fieldalignment # too strict
143 | # Settings per analyzer.
144 | settings:
145 | shadow:
146 | # Whether to be strict about shadowing; can be noisy.
147 | # Default: false
148 | strict: false
149 |
150 | lll:
151 | # Max line length, lines longer will be reported.
152 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option.
153 | # Default: 120.
154 | line-length: 120
155 | # Tab width in spaces.
156 | # Default: 1
157 | tab-width: 1
158 |
159 | nakedret:
160 | # Make an issue if func has more lines of code than this setting, and it has naked returns.
161 | # Default: 30
162 | max-func-lines: 0
163 |
164 | nestif:
165 | # Minimal complexity of if statements to report.
166 | # Default: 5
167 | min-complexity: 20
168 |
169 | nolintlint:
170 | # Exclude following linters from requiring an explanation.
171 | # Default: []
172 | allow-no-explanation: [ funlen, gocognit, lll ]
173 | # Enable to require an explanation of nonzero length after each nolint directive.
174 | # Default: false
175 | require-explanation: false
176 | # Enable to require nolint directives to mention the specific linter being suppressed.
177 | # Default: false
178 | require-specific: true
179 |
180 | rowserrcheck:
181 | # database/sql is always checked
182 | # Default: []
183 | packages:
184 | # - github.com/jmoiron/sqlx
185 |
186 | tenv:
187 | # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
188 | # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
189 | # Default: false
190 | all: true
191 |
192 |
193 | linters:
194 | disable-all: true
195 |
196 | enable:
197 | ## enabled by default
198 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
199 | - gosimple # specializes in simplifying a code
200 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
201 | - ineffassign # detects when assignments to existing variables are not used
202 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks
203 | - typecheck # like the front-end of a Go compiler, parses and type-checks Go code
204 | - unused # checks for unused constants, variables, functions and types
205 |
206 |
207 | # ## disabled by default
208 | - asasalint # checks for pass []any as any in variadic func(...any)
209 | - asciicheck # checks that your code does not contain non-ASCII identifiers
210 | - bidichk # checks for dangerous unicode character sequences
211 | - bodyclose # checks whether HTTP response body is closed successfully
212 | - cyclop # checks function and package cyclomatic complexity
213 | - dupl # tool for code clone detection
214 | - durationcheck # checks for two durations multiplied together
215 | - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
216 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
217 | - execinquery # checks query string in Query function which reads your Go src files and warning it finds
218 | #- exhaustive # checks exhaustiveness of enum switch statements
219 | - exportloopref # checks for pointers to enclosing loop variables
220 | - forbidigo # forbids identifiers
221 | - funlen # tool for detection of long functions
222 | - gochecknoinits # checks that no init functions are present in Go code
223 | - gocognit # computes and checks the cognitive complexity of functions
224 | - goconst # finds repeated strings that could be replaced by a constant
225 | - gocritic # provides diagnostics that check for bugs, performance and style issues
226 | - gocyclo # computes and checks the cyclomatic complexity of functions
227 | - godot # checks if comments end in a period
228 | - gofumpt
229 | - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
230 | # - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
231 | - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
232 | - goprintffuncname # checks that printf-like functions are named with f at the end
233 | - gosec # inspects source code for security problems
234 | #- lll # reports long lines
235 | - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
236 | - makezero # finds slice declarations with non-zero initial length
237 | - nakedret # finds naked returns in functions greater than a specified function length
238 | - nestif # reports deeply nested if statements
239 | - nilerr # finds the code that returns nil even if it checks that the error is not nil
240 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value
241 | - noctx # finds sending http request without context.Context
242 | - nolintlint # reports ill-formed or insufficient nolint directives
243 | - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
244 | - predeclared # finds code that shadows one of Go's predeclared identifiers
245 | - promlinter # checks Prometheus metrics naming via promlint
246 | - reassign # checks that package variables are not reassigned
247 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
248 | - stylecheck # is a replacement for golint
249 | - tenv # detects using os.Setenv instead of t.Setenv since Go1.17
250 | - testableexamples # checks if examples are testable (have an expected output)
251 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
252 | - unconvert # removes unnecessary type conversions
253 | - unparam # reports unused function parameters
254 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library
255 | - whitespace # detects leading and trailing whitespace
256 |
257 | ## These three linters are disabled for now due to generics: https://github.com/golangci/golangci-lint/issues/2649
258 | #- rowserrcheck # checks whether Err of rows is checked successfully # Disabled because: https://github.com/golangci/golangci-lint/issues/2649
259 | #- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
260 | #- wastedassign # finds wasted assignment statements
261 |
262 |
263 | ## you may want to enable
264 | #- decorder # checks declaration order and count of types, constants, variables and functions
265 | #- exhaustruct # checks if all structure fields are initialized
266 | #- gochecknoglobals # checks that no global variables exist
267 | #- godox # detects FIXME, TODO and other comment keywords
268 | #- goheader # checks is file header matches to pattern
269 | #- gomnd # detects magic numbers
270 | #- interfacebloat # checks the number of methods inside an interface
271 | #- ireturn # accept interfaces, return concrete types
272 | #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated
273 | #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope
274 | #- wrapcheck # checks that errors returned from external packages are wrapped
275 |
276 | ## disabled
277 | #- containedctx # detects struct contained context.Context field
278 | #- contextcheck # [too many false positives] checks the function whether use a non-inherited context
279 | #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages
280 | #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
281 | #- dupword # [useless without config] checks for duplicate words in the source code
282 | #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted
283 | #- forcetypeassert # [replaced by errcheck] finds forced type assertions
284 | #- goerr113 # [too strict] checks the errors handling expressions
285 | #- gofmt # [replaced by goimports] checks whether code was gofmt-ed
286 | #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed
287 | #- grouper # analyzes expression groups
288 | #- importas # enforces consistent import aliases
289 | #- maintidx # measures the maintainability index of each function
290 | #- misspell # [useless] finds commonly misspelled English words in comments
291 | #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity
292 | #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test
293 | #- tagliatelle # checks the struct tags
294 | #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers
295 | #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines
296 |
297 | ## deprecated
298 | #- deadcode # [deprecated, replaced by unused] finds unused code
299 | #- exhaustivestruct # [deprecated, replaced by exhaustruct] checks if all struct's fields are initialized
300 | #- golint # [deprecated, replaced by revive] golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes
301 | #- ifshort # [deprecated] checks that your code uses short syntax for if-statements whenever possible
302 | #- interfacer # [deprecated] suggests narrower interface types
303 | #- maligned # [deprecated, replaced by govet fieldalignment] detects Go structs that would take less memory if their fields were sorted
304 | #- nosnakecase # [deprecated, replaced by revive var-naming] detects snake case of variable naming and function name
305 | #- scopelint # [deprecated, replaced by exportloopref] checks for unpinned variables in go programs
306 | #- structcheck # [deprecated, replaced by unused] finds unused struct fields
307 | #- varcheck # [deprecated, replaced by unused] finds unused global variables and constants
308 |
309 |
310 | issues:
311 | # Maximum count of issues with the same text.
312 | # Set to 0 to disable.
313 | # Default: 3
314 | max-same-issues: 3
315 |
316 | exclude-rules:
317 | - source: "^//\\s*go:generate\\s"
318 | linters: [ lll ]
319 | - source: "(noinspection|TODO)"
320 | linters: [ godot ]
321 | - source: "//noinspection"
322 | linters: [ gocritic ]
323 | - source: "^\\s+if _, ok := err\\.\\([^.]+\\.InternalError\\); ok {"
324 | linters: [ errorlint ]
325 | - path: "_test\\.go"
326 | linters:
327 | - bodyclose
328 | - dupl
329 | - funlen
330 | - goconst
331 | - gosec
332 | - noctx
333 | - wrapcheck
334 |
335 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Neil O'Toole
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/neilotoole/jsoncolor/actions?query=workflow%3AGo)
2 | [](https://goreportcard.com/report/neilotoole/jsoncolor)
3 | [](https://github.com/neilotoole/jsoncolor/releases/tag/v0.7.0)
4 | [](https://pkg.go.dev/github.com/neilotoole/jsoncolor)
5 | [](./LICENSE)
6 |
7 | # jsoncolor
8 |
9 | Package `neilotoole/jsoncolor` is a drop-in replacement for stdlib
10 | [`encoding/json`](https://pkg.go.dev/encoding/json) that outputs colorized JSON.
11 |
12 | Why? Well, [`jq`](https://jqlang.github.io/jq/) colorizes its output by default, and color output
13 | is desirable for many Go CLIs. This package performs colorization (and indentation) inline
14 | in the encoder, and is significantly faster than stdlib at indentation.
15 |
16 | From the example [`jc`](./cmd/jc/main.go) app:
17 |
18 | 
19 |
20 | ## Usage
21 |
22 | Get the package per the normal mechanism (requires Go 1.16+):
23 |
24 | ```shell
25 | go get -u github.com/neilotoole/jsoncolor
26 | ```
27 |
28 | Then:
29 |
30 | ```go
31 | package main
32 |
33 | import (
34 | "fmt"
35 | "github.com/mattn/go-colorable"
36 | json "github.com/neilotoole/jsoncolor"
37 | "os"
38 | )
39 |
40 | func main() {
41 | var enc *json.Encoder
42 |
43 | // Note: this check will fail if running inside Goland (and
44 | // other IDEs?) as IsColorTerminal will return false.
45 | if json.IsColorTerminal(os.Stdout) {
46 | // Safe to use color
47 | out := colorable.NewColorable(os.Stdout) // needed for Windows
48 | enc = json.NewEncoder(out)
49 |
50 | // DefaultColors are similar to jq
51 | clrs := json.DefaultColors()
52 |
53 | // Change some values, just for fun
54 | clrs.Bool = json.Color("\x1b[36m") // Change the bool color
55 | clrs.String = json.Color{} // Disable the string color
56 |
57 | enc.SetColors(clrs)
58 | } else {
59 | // Can't use color; but the encoder will still work
60 | enc = json.NewEncoder(os.Stdout)
61 | }
62 |
63 | m := map[string]interface{}{
64 | "a": 1,
65 | "b": true,
66 | "c": "hello",
67 | }
68 |
69 | if err := enc.Encode(m); err != nil {
70 | fmt.Fprintln(os.Stderr, err)
71 | os.Exit(1)
72 | }
73 | }
74 | ```
75 |
76 | ### Configuration
77 |
78 | To enable colorization, invoke [`enc.SetColors`](https://pkg.go.dev/github.com/neilotoole/jsoncolor#Encoder.SetColors).
79 |
80 | The [`Colors`](https://pkg.go.dev/github.com/neilotoole/jsoncolor#Colors) struct
81 | holds color config. The zero value and `nil` are both safe for use (resulting in no colorization).
82 |
83 | The [`DefaultColors`](https://pkg.go.dev/github.com/neilotoole/jsoncolor#DefaultColors) func
84 | returns a `Colors` struct that produces results similar to `jq`:
85 |
86 | ```go
87 | // DefaultColors returns the default Colors configuration.
88 | // These colors largely follow jq's default colorization,
89 | // with some deviation.
90 | func DefaultColors() *Colors {
91 | return &Colors{
92 | Null: Color("\x1b[2m"),
93 | Bool: Color("\x1b[1m"),
94 | Number: Color("\x1b[36m"),
95 | String: Color("\x1b[32m"),
96 | Key: Color("\x1b[34;1m"),
97 | Bytes: Color("\x1b[2m"),
98 | Time: Color("\x1b[32;2m"),
99 | Punc: Color{}, // No colorization
100 | }
101 | }
102 | ```
103 |
104 | As seen above, use the `Color` zero value (`Color{}`) to
105 | disable colorization for that JSON element.
106 |
107 | ### Helper for `fatih/color`
108 |
109 | It can be inconvenient to use terminal codes, e.g. `json.Color("\x1b[36m")`.
110 | A helper package provides an adapter for [`fatih/color`](https://github.com/fatih/color).
111 |
112 | ```go
113 | // import "github.com/neilotoole/jsoncolor/helper/fatihcolor"
114 | // import "github.com/fatih/color"
115 | // import "github.com/mattn/go-colorable"
116 |
117 | out := colorable.NewColorable(os.Stdout) // needed for Windows
118 | enc = json.NewEncoder(out)
119 |
120 | fclrs := fatihcolor.DefaultColors()
121 | // Change some values, just for fun
122 | fclrs.Number = color.New(color.FgBlue)
123 | fclrs.String = color.New(color.FgCyan)
124 |
125 | clrs := fatihcolor.ToCoreColors(fclrs)
126 | enc.SetColors(clrs)
127 | ```
128 |
129 | ### Drop-in for `encoding/json`
130 |
131 | This package is a full drop-in for stdlib [`encoding/json`](https://pkg.go.dev/encoding/json)
132 | (thanks to the ancestral [`segmentio/encoding/json`](https://pkg.go.dev/github.com/segmentio/encoding/json)
133 | pkg being a full drop-in).
134 |
135 | To drop-in, just use an import alias:
136 |
137 | ```go
138 | import json "github.com/neilotoole/jsoncolor"
139 | ```
140 |
141 | ## Example app: `jc`
142 |
143 | See [`cmd/jc`](cmd/jc/main.go) for a trivial CLI implementation that can accept JSON input,
144 | and output that JSON in color.
145 |
146 | ```shell
147 | # From project root
148 | $ go install ./cmd/jc
149 | $ cat ./testdata/sakila_actor.json | jc
150 | ```
151 |
152 | ## Benchmarks
153 |
154 | Note that this package contains [`golang_bench_test.go`](./golang_bench_test.go), which
155 | is inherited from `segmentj`. But here we're interested in [`benchmark_test.go:BenchmarkEncode`](./benchmark_test.go),
156 | which benchmarks encoding performance versus other JSON encoder packages.
157 | The results below benchmark the following:
158 |
159 | - Stdlib [`encoding/json`](https://pkg.go.dev/encoding/json) (`go1.17.1`).
160 | - [`segmentj`](https://github.com/segmentio/encoding): `v0.1.14`, which was when `jsoncolor` was forked. The newer `segmentj` code performs even better.
161 | - `neilotoole/jsoncolor`: (this package) `v0.6.0`.
162 | - [`nwidger/jsoncolor`](https://github.com/nwidger/jsoncolor): `v0.3.0`, latest at time of benchmarks.
163 |
164 | Note that two other Go JSON colorization packages ([`hokaccha/go-prettyjson`](https://github.com/hokaccha/go-prettyjson) and
165 | [`TylerBrock/colorjson`](https://github.com/TylerBrock/colorjson)) are excluded from
166 | these benchmarks because they do not provide a stdlib-compatible `Encoder` impl.
167 |
168 | ```
169 | $ go test -bench=BenchmarkEncode -benchtime="5s"
170 | goarch: amd64
171 | pkg: github.com/neilotoole/jsoncolor
172 | cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
173 | BenchmarkEncode/stdlib_NoIndent-16 181 33047390 ns/op 8870685 B/op 120022 allocs/op
174 | BenchmarkEncode/stdlib_Indent-16 124 48093178 ns/op 10470366 B/op 120033 allocs/op
175 | BenchmarkEncode/segmentj_NoIndent-16 415 14658699 ns/op 3788911 B/op 10020 allocs/op
176 | BenchmarkEncode/segmentj_Indent-16 195 30628798 ns/op 5404492 B/op 10025 allocs/op
177 | BenchmarkEncode/neilotoole_NoIndent_NoColor-16 362 16522399 ns/op 3789034 B/op 10020 allocs/op
178 | BenchmarkEncode/neilotoole_Indent_NoColor-16 303 20146856 ns/op 5460753 B/op 10021 allocs/op
179 | BenchmarkEncode/neilotoole_NoIndent_Color-16 295 19989420 ns/op 10326019 B/op 10029 allocs/op
180 | BenchmarkEncode/neilotoole_Indent_Color-16 246 24714163 ns/op 11996890 B/op 10030 allocs/op
181 | BenchmarkEncode/nwidger_NoIndent_NoColor-16 10 541107983 ns/op 92934231 B/op 4490210 allocs/op
182 | BenchmarkEncode/nwidger_Indent_NoColor-16 7 798088086 ns/op 117258321 B/op 6290213 allocs/op
183 | BenchmarkEncode/nwidger_indent_NoIndent_Colo-16 10 542002051 ns/op 92935639 B/op 4490224 allocs/op
184 | BenchmarkEncode/nwidger_indent_Indent_Color-16 7 799928353 ns/op 117259195 B/op 6290220 allocs/op
185 | ```
186 |
187 | As always, take benchmarks with a large grain of salt, as they're based on a (small) synthetic benchmark.
188 | More benchmarks would give a better picture (and note as well that the benchmarked `segmentj` is an older version, `v0.1.14`).
189 |
190 | All that having been said, what can we surmise from these particular results?
191 |
192 | - `segmentj` performs better than `stdlib` at all encoding tasks.
193 | - `jsoncolor` performs better than `segmentj` for indentation (which makes sense, as indentation is performed inline).
194 | - `jsoncolor` performs better than `stdlib` at all encoding tasks.
195 |
196 | Again, trust these benchmarks at your peril. Create your own benchmarks for your own workload.
197 |
198 | ## Notes
199 |
200 | - The [`.golangci.yml`](./.golangci.yml) linter settings have been fiddled with to hush some
201 | linting issues inherited from the `segmentio` codebase at the time of forking. Thus, the linter report
202 | may not be of great use. In an ideal world, the `jsoncolor` functionality would be [ported](https://github.com/neilotoole/jsoncolor/issues/15) to a
203 | more recent (and better-linted) version of the `segementio` codebase.
204 | - The `segmentio` encoder (at least as of `v0.1.14`) encodes `time.Duration` as string, while `stdlib` outputs as `int64`.
205 | This package follows `stdlib`.
206 | - The [`Colors.Punc`](https://pkg.go.dev/github.com/neilotoole/jsoncolor#Colors) field controls all
207 | punctuation colorization, i.e. `[]{},:"`. It is probably worthwhile to [separate](https://github.com/neilotoole/jsoncolor/issues/16)
208 | these out into individually-configurable elements.
209 |
210 |
211 | ## CHANGELOG
212 |
213 | History: this package is an extract of [`sq`](https://github.com/neilotoole/sq)'s JSON encoding package, which itself is a fork of the
214 | [`segmentio/encoding`](https://github.com/segmentio/encoding) JSON encoding package. Note that the
215 | original `sq` JSON encoder was forked from Segment's codebase at `v0.1.14`, so
216 | the codebases have drifted significantly by now.
217 |
218 | ### [v0.7.1](https://github.com/neilotoole/jsoncolor/releases/tag/v0.7.1)
219 |
220 | - [#27](https://github.com/neilotoole/jsoncolor/pull/27): Improved Windows terminal color support checking.
221 |
222 | ### [v0.7.0](https://github.com/neilotoole/jsoncolor/releases/tag/v0.7.0)
223 |
224 | - [#21](https://github.com/neilotoole/jsoncolor/pull/21): Support for [`encoding.TextMarshaler`](https://pkg.go.dev/encoding#TextMarshaler).
225 | - [#22](https://github.com/neilotoole/jsoncolor/pull/22): Removed redundant dependencies.
226 | - [#26](https://github.com/neilotoole/jsoncolor/pull/26): Updated dependencies.
227 |
228 | ## Acknowledgments
229 |
230 | - [`jq`](https://stedolan.github.io/jq/): sine qua non.
231 | - [`segmentio/encoding`](https://github.com/segmentio/encoding): `jsoncolor` is layered into Segment's JSON encoder. They did the hard work. Much gratitude to that team.
232 | - [`sq`](https://github.com/neilotoole/sq): `jsoncolor` is effectively an extract of code created specifically for `sq`.
233 | - [`mattn/go-colorable`](https://github.com/mattn/go-colorable): no project is complete without `mattn` having played a role.
234 | - [`fatih/color`](https://github.com/fatih/color): the color library.
235 | - [`@hermannm`](https://github.com/hermannm): for several PRs.
236 |
237 | ### Related
238 |
239 | - [`nwidger/jsoncolor`](https://github.com/nwidger/jsoncolor)
240 | - [`hokaccha/go-prettyjson`](https://github.com/hokaccha/go-prettyjson)
241 | - [`TylerBrock/colorjson`](https://github.com/TylerBrock/colorjson)
242 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 |
6 | | Version | Supported |
7 | | ------- | ------------------ |
8 | | v0.7.0 | :white_check_mark: |
9 | | v0.6.0 | :x: |
10 |
11 |
12 | ## Reporting a Vulnerability
13 |
14 | Open an [issue](https://github.com/neilotoole/jsoncolor/issues/new).
15 |
--------------------------------------------------------------------------------
/SEGMENTIO_README.md:
--------------------------------------------------------------------------------
1 | # encoding/json [](https://godoc.org/github.com/segmentio/encoding/json)
2 |
3 | Go package offering a replacement implementation of the standard library's
4 | [`encoding/json`](https://golang.org/pkg/encoding/json/) package, with much
5 | better performance.
6 |
7 | ## Usage
8 |
9 | The exported API of this package mirrors the standard library's
10 | [`encoding/json`](https://golang.org/pkg/encoding/json/) package, the only
11 | change needed to take advantage of the performance improvements is the import
12 | path of the `json` package, from:
13 | ```go
14 | import (
15 | "encoding/json"
16 | )
17 | ```
18 | to
19 | ```go
20 | import (
21 | "github.com/segmentio/encoding/json"
22 | )
23 | ```
24 |
25 | One way to gain higher encoding throughput is to disable HTML escaping.
26 | It allows the string encoding to use a much more efficient code path which
27 | does not require parsing UTF-8 runes most of the time.
28 |
29 | ## Performance Improvements
30 |
31 | The internal implementation uses a fair amount of unsafe operations (untyped
32 | code, pointer arithmetic, etc...) to avoid using reflection as much as possible,
33 | which is often the reason why serialization code has a large CPU and memory
34 | footprint.
35 |
36 | The package aims for zero unnecessary dynamic memory allocations and hot code
37 | paths that are mostly free from calls into the reflect package.
38 |
39 | ## Compatibility with encoding/json
40 |
41 | This package aims to be a drop-in replacement, therefore it is tested to behave
42 | exactly like the standard library's package. However, there are still a few
43 | missing features that have not been ported yet:
44 |
45 | - Streaming decoder, currently the `Decoder` implementation offered by the
46 | package does not support progressively reading values from a JSON array (unlike
47 | the standard library). In our experience this is a very rare use-case, if you
48 | need it you're better off sticking to the standard library, or spend a bit of
49 | time implementing it in here ;)
50 |
51 | Note that none of those features should result in performance degradations if
52 | they were implemented in the package, and we welcome contributions!
53 |
54 | ## Trade-offs
55 |
56 | As one would expect, we had to make a couple of trade-offs to achieve greater
57 | performance than the standard library, but there were also features that we
58 | did not want to give away.
59 |
60 | Other open-source packages offering a reduced CPU and memory footprint usually
61 | do so by designing a different API, or require code generation (therefore adding
62 | complexity to the build process). These were not acceptable conditions for us,
63 | as we were not willing to trade off developer productivity for better runtime
64 | performance. To achieve this, we chose to exactly replicate the standard
65 | library interfaces and behavior, which meant the package implementation was the
66 | only area that we were able to work with. The internals of this package make
67 | heavy use of unsafe pointer arithmetics and other performance optimizations,
68 | and therefore are not as approachable as typical Go programs. Basically, we put
69 | a bigger burden on maintainers to achieve better runtime cost without
70 | sacrificing developer productivity.
71 |
72 | For these reasons, we also don't believe that this code should be ported upstream
73 | to the standard `encoding/json` package. The standard library has to remain
74 | readable and approachable to maximize stability and maintainability, and make
75 | projects like this one possible because a high quality reference implementation
76 | already exists.
77 |
--------------------------------------------------------------------------------
/ascii.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import "unsafe"
4 |
5 | // asciiValid returns true if b contains only ASCII characters.
6 | //
7 | // From https://github.com/segmentio/encoding/blob/v0.1.14/ascii/valid.go#L28
8 | //
9 | //go:nosplit
10 | func asciiValid(b []byte) bool {
11 | s, n := unsafe.Pointer(&b), uintptr(len(b))
12 |
13 | i := uintptr(0)
14 | p := *(*unsafe.Pointer)(s)
15 |
16 | for n >= 8 {
17 | if ((*(*uint64)(unsafe.Pointer(uintptr(p) + i))) & 0x8080808080808080) != 0 {
18 | return false
19 | }
20 | i += 8
21 | n -= 8
22 | }
23 |
24 | if n >= 4 {
25 | if ((*(*uint32)(unsafe.Pointer(uintptr(p) + i))) & 0x80808080) != 0 {
26 | return false
27 | }
28 | i += 4
29 | n -= 4
30 | }
31 |
32 | var x uint32
33 | switch n {
34 | case 3:
35 | x = uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) | uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i + 1)))<<8
36 | case 2:
37 | x = uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i)))
38 | case 1:
39 | x = uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i)))
40 | default:
41 | return true
42 | }
43 | return (x & 0x80808080) == 0
44 | }
45 |
46 | // asciiValidPrint returns true if b contains only printable ASCII characters.
47 | //
48 | // From https://github.com/segmentio/encoding/blob/v0.1.14/ascii/valid.go#L83
49 | //
50 | //go:nosplit
51 | func asciiValidPrint(b []byte) bool {
52 | s, n := unsafe.Pointer(&b), uintptr(len(b))
53 |
54 | if n == 0 {
55 | return true
56 | }
57 |
58 | i := uintptr(0)
59 | p := *(*unsafe.Pointer)(s)
60 |
61 | for (n - i) >= 8 {
62 | x := *(*uint64)(unsafe.Pointer(uintptr(p) + i))
63 | if hasLess64(x, 0x20) || hasMore64(x, 0x7e) {
64 | return false
65 | }
66 | i += 8
67 | }
68 |
69 | if (n - i) >= 4 {
70 | x := *(*uint32)(unsafe.Pointer(uintptr(p) + i))
71 | if hasLess32(x, 0x20) || hasMore32(x, 0x7e) {
72 | return false
73 | }
74 | i += 4
75 | }
76 |
77 | var x uint32
78 | switch n - i {
79 | case 3:
80 | x = 0x20000000 | uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i))) | uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i + 1)))<<8
81 | case 2:
82 | x = 0x20200000 | uint32(*(*uint16)(unsafe.Pointer(uintptr(p) + i)))
83 | case 1:
84 | x = 0x20202000 | uint32(*(*uint8)(unsafe.Pointer(uintptr(p) + i)))
85 | default:
86 | return true
87 | }
88 | return !(hasLess32(x, 0x20) || hasMore32(x, 0x7e))
89 | }
90 |
91 | // https://graphics.stanford.edu/~seander/bithacks.html#HasLessInWord
92 | const (
93 | hasLessConstL64 = (^uint64(0)) / 255
94 | hasLessConstR64 = hasLessConstL64 * 128
95 |
96 | hasLessConstL32 = (^uint32(0)) / 255
97 | hasLessConstR32 = hasLessConstL32 * 128
98 |
99 | hasMoreConstL64 = (^uint64(0)) / 255
100 | hasMoreConstR64 = hasMoreConstL64 * 128
101 |
102 | hasMoreConstL32 = (^uint32(0)) / 255
103 | hasMoreConstR32 = hasMoreConstL32 * 128
104 | )
105 |
106 | //go:nosplit
107 | func hasLess64(x, n uint64) bool {
108 | return ((x - (hasLessConstL64 * n)) & ^x & hasLessConstR64) != 0
109 | }
110 |
111 | //go:nosplit
112 | func hasLess32(x, n uint32) bool {
113 | return ((x - (hasLessConstL32 * n)) & ^x & hasLessConstR32) != 0
114 | }
115 |
116 | //go:nosplit
117 | func hasMore64(x, n uint64) bool {
118 | return (((x + (hasMoreConstL64 * (127 - n))) | x) & hasMoreConstR64) != 0
119 | }
120 |
121 | //go:nosplit
122 | func hasMore32(x, n uint32) bool {
123 | return (((x + (hasMoreConstL32 * (127 - n))) | x) & hasMoreConstR32) != 0
124 | }
125 |
--------------------------------------------------------------------------------
/ascii_test.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import (
4 | "strings"
5 | "testing"
6 | )
7 |
8 | // Based on https://github.com/segmentio/encoding/blob/v0.1.14/ascii/valid_test.go
9 | var testCases = [...]struct {
10 | valid bool
11 | validPrint bool
12 | str string
13 | }{
14 | {valid: true, validPrint: true, str: ""},
15 | {valid: true, validPrint: true, str: "hello"},
16 | {valid: true, validPrint: true, str: "Hello World!"},
17 | {valid: true, validPrint: true, str: "Hello\"World!"},
18 | {valid: true, validPrint: true, str: "Hello\\World!"},
19 | {valid: true, validPrint: false, str: "Hello\nWorld!"},
20 | {valid: true, validPrint: false, str: "Hello\rWorld!"},
21 | {valid: true, validPrint: false, str: "Hello\tWorld!"},
22 | {valid: true, validPrint: false, str: "Hello\bWorld!"},
23 | {valid: true, validPrint: false, str: "Hello\fWorld!"},
24 | {valid: true, validPrint: true, str: "H~llo World!"},
25 | {valid: true, validPrint: true, str: "H~llo"},
26 | {valid: false, validPrint: false, str: "你好"},
27 | {valid: true, validPrint: true, str: "~"},
28 | {valid: false, validPrint: false, str: "\x80"},
29 | {valid: true, validPrint: false, str: "\x7F"},
30 | {valid: false, validPrint: false, str: "\xFF"},
31 | {valid: true, validPrint: true, str: "some kind of long string with only ascii characters."},
32 | {valid: false, validPrint: false, str: "some kind of long string with a non-ascii character at the end.\xff"},
33 | {valid: true, validPrint: true, str: strings.Repeat("1234567890", 1000)},
34 | }
35 |
36 | func TestAsciiValid(t *testing.T) {
37 | for _, tc := range testCases {
38 | t.Run(limit(tc.str), func(t *testing.T) {
39 | expect := tc.validPrint
40 |
41 | if valid := asciiValidPrint([]byte(tc.str)); expect != valid {
42 | t.Errorf("expected %t but got %t", expect, valid)
43 | }
44 | })
45 | }
46 | }
47 |
48 | func TestAsciiValidPrint(t *testing.T) {
49 | for _, tc := range testCases {
50 | t.Run(limit(tc.str), func(t *testing.T) {
51 | expect := tc.validPrint
52 |
53 | if valid := asciiValidPrint([]byte(tc.str)); expect != valid {
54 | t.Errorf("expected %t but got %t", expect, valid)
55 | }
56 | })
57 | }
58 | }
59 |
60 | func limit(s string) string {
61 | if len(s) > 17 {
62 | return s[:17] + "..."
63 | }
64 | return s
65 | }
66 |
--------------------------------------------------------------------------------
/benchmark_test.go:
--------------------------------------------------------------------------------
1 | package jsoncolor_test
2 |
3 | import (
4 | "bytes"
5 | stdj "encoding/json"
6 | "io"
7 | "io/ioutil"
8 | "testing"
9 | "time"
10 |
11 | segmentj "github.com/segmentio/encoding/json"
12 |
13 | "github.com/neilotoole/jsoncolor"
14 | nwidgerj "github.com/nwidger/jsoncolor"
15 | )
16 |
17 | func BenchmarkEncode(b *testing.B) {
18 | recs := makeRecords(b, 10000)
19 |
20 | benchmarks := []struct {
21 | name string
22 | indent bool
23 | color bool
24 | fn newEncoderFunc
25 | }{
26 | {name: "stdlib_NoIndent", fn: newEncStdlib},
27 | {name: "stdlib_Indent", fn: newEncStdlib, indent: true},
28 | {name: "segmentj_NoIndent", fn: newEncSegmentj},
29 | {name: "segmentj_Indent", fn: newEncSegmentj, indent: true},
30 | {name: "neilotoole_NoIndent_NoColor", fn: newEncNeilotoole},
31 | {name: "neilotoole_Indent_NoColor", fn: newEncNeilotoole, indent: true},
32 | {name: "neilotoole_NoIndent_Color", fn: newEncNeilotoole, color: true},
33 | {name: "neilotoole_Indent_Color", fn: newEncNeilotoole, indent: true, color: true},
34 | {name: "nwidger_NoIndent_NoColor", fn: newEncNwidger},
35 | {name: "nwidger_Indent_NoColor", fn: newEncNwidger, indent: true},
36 | {name: "nwidger_indent_NoIndent_Colo", fn: newEncNwidger, color: true},
37 | {name: "nwidger_indent_Indent_Color", fn: newEncNwidger, indent: true, color: true},
38 | }
39 |
40 | for _, bm := range benchmarks {
41 | bm := bm
42 | b.Run(bm.name, func(b *testing.B) {
43 | b.ReportAllocs()
44 |
45 | b.ResetTimer()
46 |
47 | for n := 0; n < b.N; n++ {
48 | w := &bytes.Buffer{}
49 | enc := bm.fn(w, bm.indent, bm.color)
50 |
51 | for i := range recs {
52 | err := enc.Encode(recs[i])
53 | if err != nil {
54 | b.Error(err)
55 | }
56 | }
57 | }
58 | })
59 | }
60 | }
61 |
62 | func makeRecords(tb testing.TB, n int) [][]interface{} {
63 | recs := make([][]interface{}, 0, n)
64 |
65 | // add a bunch of data from a file, just to make the recs bigger
66 | data, err := ioutil.ReadFile("testdata/sakila_actor.json")
67 | if err != nil {
68 | tb.Fatal(err)
69 | }
70 |
71 | f := new(interface{})
72 | if err = stdj.Unmarshal(data, f); err != nil {
73 | tb.Fatal(err)
74 | }
75 |
76 | type someStruct struct {
77 | i int64
78 | a string
79 | f interface{} // x holds JSON loaded from file
80 | }
81 |
82 | for i := 0; i < n; i++ {
83 | rec := []interface{}{
84 | int(1),
85 | int64(2),
86 | float32(2.71),
87 | float64(3.14),
88 | "hello world",
89 | someStruct{i: 8, a: "goodbye world", f: f},
90 | map[string]interface{}{"a": 9, "b": "ca va"},
91 | true,
92 | false,
93 | time.Unix(1631659220, 0),
94 | time.Millisecond * 1631659220,
95 | }
96 | recs = append(recs, rec)
97 | }
98 |
99 | return recs
100 | }
101 |
102 | type newEncoderFunc func(w io.Writer, indent, color bool) encoder
103 |
104 | var (
105 | _ newEncoderFunc = newEncStdlib
106 | _ newEncoderFunc = newEncSegmentj
107 | _ newEncoderFunc = newEncNeilotoole
108 | _ newEncoderFunc = newEncNwidger
109 | )
110 |
111 | type encoder interface {
112 | SetEscapeHTML(on bool)
113 | SetIndent(prefix, indent string)
114 | Encode(v interface{}) error
115 | }
116 |
117 | func newEncStdlib(w io.Writer, indent, color bool) encoder {
118 | enc := stdj.NewEncoder(w)
119 | if indent {
120 | enc.SetIndent("", " ")
121 | }
122 | enc.SetEscapeHTML(true)
123 | return enc
124 | }
125 |
126 | func newEncSegmentj(w io.Writer, indent, color bool) encoder {
127 | enc := segmentj.NewEncoder(w)
128 | if indent {
129 | enc.SetIndent("", " ")
130 | }
131 | enc.SetEscapeHTML(true)
132 | return enc
133 | }
134 |
135 | func newEncNeilotoole(w io.Writer, indent, color bool) encoder {
136 | enc := jsoncolor.NewEncoder(w)
137 | if indent {
138 | enc.SetIndent("", " ")
139 | }
140 | enc.SetEscapeHTML(true)
141 |
142 | if color {
143 | clrs := jsoncolor.DefaultColors()
144 | enc.SetColors(clrs)
145 | }
146 |
147 | return enc
148 | }
149 |
150 | func newEncNwidger(w io.Writer, indent, color bool) encoder {
151 | if !color {
152 | enc := nwidgerj.NewEncoder(w)
153 | enc.SetEscapeHTML(false)
154 | if indent {
155 | enc.SetIndent("", " ")
156 | }
157 | return enc
158 | }
159 |
160 | // It's color
161 | f := nwidgerj.NewFormatter()
162 | f.SpaceColor = nwidgerj.DefaultSpaceColor
163 | f.CommaColor = nwidgerj.DefaultCommaColor
164 | f.ColonColor = nwidgerj.DefaultColonColor
165 | f.ObjectColor = nwidgerj.DefaultObjectColor
166 | f.ArrayColor = nwidgerj.DefaultArrayColor
167 | f.FieldQuoteColor = nwidgerj.DefaultFieldQuoteColor
168 | f.FieldColor = nwidgerj.DefaultFieldColor
169 | f.StringQuoteColor = nwidgerj.DefaultStringQuoteColor
170 | f.StringColor = nwidgerj.DefaultStringColor
171 | f.TrueColor = nwidgerj.DefaultTrueColor
172 | f.FalseColor = nwidgerj.DefaultFalseColor
173 | f.NumberColor = nwidgerj.DefaultNumberColor
174 | f.NullColor = nwidgerj.DefaultNullColor
175 |
176 | enc := nwidgerj.NewEncoderWithFormatter(w, f)
177 | enc.SetEscapeHTML(false)
178 |
179 | if indent {
180 | enc.SetIndent("", " ")
181 | }
182 |
183 | return enc
184 | }
185 |
--------------------------------------------------------------------------------
/cmd/jc/main.go:
--------------------------------------------------------------------------------
1 | // Package main contains a trivial CLI that accepts JSON input either
2 | // via stdin or via "-i path/to/input.json", and outputs JSON
3 | // to stdout, or if "-o path/to/output.json" is set, outputs to that file.
4 | // If -c (colorized) is true, output to stdout will be colorized if possible
5 | // (but never colorized for file output).
6 | //
7 | // Examples:
8 | //
9 | // $ cat example.json | jc
10 | // $ cat example.json | jc -c false
11 | package main
12 |
13 | import (
14 | "errors"
15 | "flag"
16 | "fmt"
17 | "io"
18 | "io/ioutil"
19 | "os"
20 | "path/filepath"
21 |
22 | "github.com/mattn/go-colorable"
23 | json "github.com/neilotoole/jsoncolor"
24 | )
25 |
26 | var (
27 | flagPretty = flag.Bool("p", true, "output pretty JSON")
28 | flagColorize = flag.Bool("c", true, "output colorized JSON")
29 | flagInputFile = flag.String("i", "", "path to input JSON file")
30 | flagOutputFile = flag.String("o", "", "path to output JSON file")
31 | )
32 |
33 | func printUsage() {
34 | const msg = `
35 | jc (jsoncolor) is a trivial CLI to demonstrate the neilotoole/jsoncolor package.
36 | It accepts JSON input, and outputs colorized, prettified JSON.
37 |
38 | Example Usage:
39 |
40 | # Pipe a JSON file, using defaults (colorized and prettified); print to stdout
41 | $ cat testdata/sakila_actor.json | jc
42 |
43 | # Read input from a JSON file, print to stdout, DO colorize but DO NOT prettify
44 | $ jc -c -p=false -i ./testdata/sakila_actor.json
45 |
46 | # Pipe a JSON input file to jc, outputting to a specified file; and DO NOT prettify
47 | $ cat ./testdata/sakila_actor.json | jc -p=false -o /tmp/out.json`
48 | fmt.Fprintln(os.Stderr, msg)
49 | }
50 |
51 | func main() {
52 | flag.Parse()
53 | if err := doMain(); err != nil {
54 | fmt.Fprintf(os.Stderr, "error: %v\n", err)
55 | printUsage()
56 | os.Exit(1)
57 | }
58 | }
59 |
60 | func doMain() error {
61 | var (
62 | input []byte
63 | err error
64 | )
65 |
66 | if flagInputFile != nil && *flagInputFile != "" {
67 | // Read from file
68 | var f *os.File
69 | if f, err = os.Open(*flagInputFile); err != nil {
70 | return err
71 | }
72 | defer f.Close()
73 |
74 | if input, err = ioutil.ReadAll(f); err != nil {
75 | return err
76 | }
77 | } else {
78 | // Probably read from stdin...
79 | var fi os.FileInfo
80 | if fi, err = os.Stdin.Stat(); err != nil {
81 | return err
82 | }
83 |
84 | if (fi.Mode() & os.ModeCharDevice) == 0 {
85 | // Read from stdin
86 | if input, err = ioutil.ReadAll(os.Stdin); err != nil {
87 | return err
88 | }
89 | } else {
90 | return errors.New("invalid args")
91 | }
92 | }
93 |
94 | jsn := new(interface{}) // generic interface{} that will hold the parsed JSON
95 | if err = json.Unmarshal(input, jsn); err != nil {
96 | return fmt.Errorf("invalid input JSON: %w", err)
97 | }
98 |
99 | var out io.Writer
100 | if flagOutputFile != nil && *flagOutputFile != "" {
101 | // Output file is specified via -o flag
102 | var fpath string
103 | if fpath, err = filepath.Abs(*flagOutputFile); err != nil {
104 | return fmt.Errorf("failed to get absolute path for -o %q: %w", *flagOutputFile, err)
105 | }
106 |
107 | // Ensure the parent dir exists
108 | if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
109 | return fmt.Errorf("failed to make parent dir for -o %q: %w", *flagOutputFile, err)
110 | }
111 |
112 | var f *os.File
113 | if f, err = os.Create(fpath); err != nil {
114 | return fmt.Errorf("failed to open output file specified by -o %q: %w", *flagOutputFile, err)
115 | }
116 | defer f.Close()
117 | out = f
118 | } else {
119 | // Output file NOT specified via -o flag, use stdout.
120 | out = os.Stdout
121 | }
122 |
123 | var enc *json.Encoder
124 |
125 | if flagColorize != nil && *flagColorize && json.IsColorTerminal(out) {
126 | out = colorable.NewColorable(out.(*os.File)) // colorable is needed for Windows
127 | enc = json.NewEncoder(out)
128 | clrs := json.DefaultColors()
129 | enc.SetColors(clrs)
130 | } else {
131 | // We are NOT doing color output: either flag not set, or we
132 | // could be outputting to a file etc.
133 | // Therefore DO NOT call enc.SetColors.
134 | enc = json.NewEncoder(out)
135 | }
136 |
137 | if flagPretty != nil && *flagPretty {
138 | // Pretty-print, i.e. set indent
139 | enc.SetIndent("", " ")
140 | }
141 |
142 | return enc.Encode(jsn)
143 | }
144 |
--------------------------------------------------------------------------------
/encode.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import (
4 | "bytes"
5 | "encoding"
6 | "encoding/base64"
7 | "errors"
8 | "math"
9 | "reflect"
10 | "sort"
11 | "strconv"
12 | "sync"
13 | "time"
14 | "unicode/utf8"
15 | "unsafe"
16 | )
17 |
18 | const hex = "0123456789abcdef"
19 |
20 | func (e encoder) encodeNull(b []byte, _ unsafe.Pointer) ([]byte, error) {
21 | return e.clrs.appendNull(b), nil
22 | }
23 |
24 | func (e encoder) encodeBool(b []byte, p unsafe.Pointer) ([]byte, error) {
25 | return e.clrs.appendBool(b, *(*bool)(p)), nil
26 | }
27 |
28 | func (e encoder) encodeInt(b []byte, p unsafe.Pointer) ([]byte, error) {
29 | return e.clrs.appendInt64(b, int64(*(*int)(p))), nil
30 | }
31 |
32 | func (e encoder) encodeInt8(b []byte, p unsafe.Pointer) ([]byte, error) {
33 | return e.clrs.appendInt64(b, int64(*(*int8)(p))), nil
34 | }
35 |
36 | func (e encoder) encodeInt16(b []byte, p unsafe.Pointer) ([]byte, error) {
37 | return e.clrs.appendInt64(b, int64(*(*int16)(p))), nil
38 | }
39 |
40 | func (e encoder) encodeInt32(b []byte, p unsafe.Pointer) ([]byte, error) {
41 | return e.clrs.appendInt64(b, int64(*(*int32)(p))), nil
42 | }
43 |
44 | func (e encoder) encodeInt64(b []byte, p unsafe.Pointer) ([]byte, error) {
45 | return e.clrs.appendInt64(b, *(*int64)(p)), nil
46 | }
47 |
48 | func (e encoder) encodeUint(b []byte, p unsafe.Pointer) ([]byte, error) {
49 | return e.clrs.appendUint64(b, uint64(*(*uint)(p))), nil
50 | }
51 |
52 | func (e encoder) encodeUintptr(b []byte, p unsafe.Pointer) ([]byte, error) {
53 | return e.clrs.appendUint64(b, uint64(*(*uintptr)(p))), nil
54 | }
55 |
56 | func (e encoder) encodeUint8(b []byte, p unsafe.Pointer) ([]byte, error) {
57 | return e.clrs.appendUint64(b, uint64(*(*uint8)(p))), nil
58 | }
59 |
60 | func (e encoder) encodeUint16(b []byte, p unsafe.Pointer) ([]byte, error) {
61 | return e.clrs.appendUint64(b, uint64(*(*uint16)(p))), nil
62 | }
63 |
64 | func (e encoder) encodeUint32(b []byte, p unsafe.Pointer) ([]byte, error) {
65 | return e.clrs.appendUint64(b, uint64(*(*uint32)(p))), nil
66 | }
67 |
68 | func (e encoder) encodeUint64(b []byte, p unsafe.Pointer) ([]byte, error) {
69 | return e.clrs.appendUint64(b, *(*uint64)(p)), nil
70 | }
71 |
72 | func (e encoder) encodeFloat32(b []byte, p unsafe.Pointer) ([]byte, error) {
73 | if e.clrs == nil {
74 | return e.encodeFloat(b, float64(*(*float32)(p)), 32)
75 | }
76 |
77 | b = append(b, e.clrs.Number...)
78 | var err error
79 | b, err = e.encodeFloat(b, float64(*(*float32)(p)), 32)
80 | b = append(b, ansiReset...)
81 | return b, err
82 | }
83 |
84 | func (e encoder) encodeFloat64(b []byte, p unsafe.Pointer) ([]byte, error) {
85 | if e.clrs == nil {
86 | return e.encodeFloat(b, *(*float64)(p), 64)
87 | }
88 |
89 | b = append(b, e.clrs.Number...)
90 | var err error
91 | b, err = e.encodeFloat(b, *(*float64)(p), 64)
92 | b = append(b, ansiReset...)
93 | return b, err
94 | }
95 |
96 | func (e encoder) encodeFloat(b []byte, f float64, bits int) ([]byte, error) {
97 | switch {
98 | case math.IsNaN(f):
99 | return b, &UnsupportedValueError{Value: reflect.ValueOf(f), Str: "NaN"}
100 | case math.IsInf(f, 0):
101 | return b, &UnsupportedValueError{Value: reflect.ValueOf(f), Str: "inf"}
102 | }
103 |
104 | // Convert as if by ES6 number to string conversion.
105 | // This matches most other JSON generators.
106 | // See golang.org/issue/6384 and golang.org/issue/14135.
107 | // Like fmt %g, but the exponent cutoffs are different
108 | // and exponents themselves are not padded to two digits.
109 | abs := math.Abs(f)
110 | fmt := byte('f')
111 | // Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
112 | if abs != 0 {
113 | if bits == 64 && (abs < 1e-6 || abs >= 1e21) || bits == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
114 | fmt = 'e'
115 | }
116 | }
117 |
118 | b = strconv.AppendFloat(b, f, fmt, -1, bits)
119 |
120 | if fmt == 'e' {
121 | // clean up e-09 to e-9
122 | n := len(b)
123 | if n >= 4 && b[n-4] == 'e' && b[n-3] == '-' && b[n-2] == '0' {
124 | b[n-2] = b[n-1]
125 | b = b[:n-1]
126 | }
127 | }
128 |
129 | return b, nil
130 | }
131 |
132 | func (e encoder) encodeNumber(b []byte, p unsafe.Pointer) ([]byte, error) {
133 | n := *(*Number)(p)
134 | if n == "" {
135 | n = "0"
136 | }
137 |
138 | _, _, err := parseNumber(stringToBytes(string(n)))
139 | if err != nil {
140 | return b, err
141 | }
142 |
143 | if e.clrs == nil {
144 | return append(b, n...), nil
145 | }
146 |
147 | b = append(b, e.clrs.Number...)
148 | b = append(b, n...)
149 | b = append(b, ansiReset...)
150 | return b, nil
151 | }
152 |
153 | func (e encoder) encodeKey(b []byte, p unsafe.Pointer) ([]byte, error) {
154 | if e.clrs == nil {
155 | return e.doEncodeString(b, p)
156 | }
157 |
158 | b = append(b, e.clrs.Key...)
159 | var err error
160 | b, err = e.doEncodeString(b, p)
161 | b = append(b, ansiReset...)
162 | return b, err
163 | }
164 |
165 | func (e encoder) encodeString(b []byte, p unsafe.Pointer) ([]byte, error) {
166 | if e.clrs == nil {
167 | return e.doEncodeString(b, p)
168 | }
169 |
170 | b = append(b, e.clrs.String...)
171 | var err error
172 | b, err = e.doEncodeString(b, p)
173 | b = append(b, ansiReset...)
174 | return b, err
175 | }
176 |
177 | func (e encoder) doEncodeString(b []byte, p unsafe.Pointer) ([]byte, error) {
178 | s := *(*string)(p)
179 | i := 0
180 | j := 0
181 | escapeHTML := (e.flags & EscapeHTML) != 0
182 |
183 | b = append(b, '"')
184 |
185 | for j < len(s) {
186 | c := s[j]
187 |
188 | if c >= 0x20 && c <= 0x7f && c != '\\' && c != '"' && (!escapeHTML || (c != '<' && c != '>' && c != '&')) {
189 | // fast path: most of the time, printable ascii characters are used
190 | j++
191 | continue
192 | }
193 |
194 | switch c {
195 | case '\\', '"':
196 | b = append(b, s[i:j]...)
197 | b = append(b, '\\', c)
198 | i = j + 1
199 | j = i
200 | continue
201 |
202 | case '\n':
203 | b = append(b, s[i:j]...)
204 | b = append(b, '\\', 'n')
205 | i = j + 1
206 | j = i
207 | continue
208 |
209 | case '\r':
210 | b = append(b, s[i:j]...)
211 | b = append(b, '\\', 'r')
212 | i = j + 1
213 | j = i
214 | continue
215 |
216 | case '\t':
217 | b = append(b, s[i:j]...)
218 | b = append(b, '\\', 't')
219 | i = j + 1
220 | j = i
221 | continue
222 |
223 | case '<', '>', '&':
224 | b = append(b, s[i:j]...)
225 | b = append(b, `\u00`...)
226 | b = append(b, hex[c>>4], hex[c&0xF])
227 | i = j + 1
228 | j = i
229 | continue
230 | }
231 |
232 | // This encodes bytes < 0x20 except for \t, \n and \r.
233 | if c < 0x20 {
234 | b = append(b, s[i:j]...)
235 | b = append(b, `\u00`...)
236 | b = append(b, hex[c>>4], hex[c&0xF])
237 | i = j + 1
238 | j = i
239 | continue
240 | }
241 |
242 | r, size := utf8.DecodeRuneInString(s[j:])
243 |
244 | if r == utf8.RuneError && size == 1 {
245 | b = append(b, s[i:j]...)
246 | b = append(b, `\ufffd`...)
247 | i = j + size
248 | j = i
249 | continue
250 | }
251 |
252 | switch r {
253 | case '\u2028', '\u2029':
254 | // U+2028 is LINE SEPARATOR.
255 | // U+2029 is PARAGRAPH SEPARATOR.
256 | // They are both technically valid characters in JSON strings,
257 | // but don't work in JSONP, which has to be evaluated as JavaScript,
258 | // and can lead to security holes there. It is valid JSON to
259 | // escape them, so we do so unconditionally.
260 | // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
261 | b = append(b, s[i:j]...)
262 | b = append(b, `\u202`...)
263 | b = append(b, hex[r&0xF])
264 | i = j + size
265 | j = i
266 | continue
267 | }
268 |
269 | j += size
270 | }
271 |
272 | b = append(b, s[i:]...)
273 | b = append(b, '"')
274 | return b, nil
275 | }
276 |
277 | func (e encoder) encodeToString(b []byte, p unsafe.Pointer, encode encodeFunc) ([]byte, error) {
278 | i := len(b)
279 |
280 | b, err := encode(e, b, p)
281 | if err != nil {
282 | return b, err
283 | }
284 |
285 | j := len(b)
286 | s := b[i:]
287 |
288 | if b, err = e.doEncodeString(b, unsafe.Pointer(&s)); err != nil {
289 | return b, err
290 | }
291 |
292 | n := copy(b[i:], b[j:])
293 | return b[:i+n], nil
294 | }
295 |
296 | func (e encoder) encodeBytes(b []byte, p unsafe.Pointer) ([]byte, error) {
297 | if e.clrs == nil {
298 | return e.doEncodeBytes(b, p)
299 | }
300 |
301 | b = append(b, e.clrs.Bytes...)
302 | var err error
303 | b, err = e.doEncodeBytes(b, p)
304 | return append(b, ansiReset...), err
305 | }
306 |
307 | func (e encoder) doEncodeBytes(b []byte, p unsafe.Pointer) ([]byte, error) {
308 | v := *(*[]byte)(p)
309 | if v == nil {
310 | return e.clrs.appendNull(b), nil
311 | }
312 |
313 | n := base64.StdEncoding.EncodedLen(len(v)) + 2
314 |
315 | if avail := cap(b) - len(b); avail < n {
316 | newB := make([]byte, cap(b)+(n-avail))
317 | copy(newB, b)
318 | b = newB[:len(b)]
319 | }
320 |
321 | i := len(b)
322 | j := len(b) + n
323 |
324 | b = b[:j]
325 | b[i] = '"'
326 | base64.StdEncoding.Encode(b[i+1:j-1], v)
327 | b[j-1] = '"'
328 | return b, nil
329 | }
330 |
331 | func (e encoder) encodeDuration(b []byte, p unsafe.Pointer) ([]byte, error) {
332 | // NOTE: The segmentj encoder does special handling for time.Duration (converts to string).
333 | // The stdlib encoder does not. It just outputs the int64 value.
334 | // We choose to follow the stdlib pattern, for fuller compatibility.
335 |
336 | b = e.clrs.appendInt64(b, int64(*(*time.Duration)(p)))
337 | return b, nil
338 |
339 | // NOTE: if we were to follow the segmentj pattern, we'd execute the code below.
340 | // if e.clrs == nil {
341 | // b = append(b, '"')
342 | //
343 | // b = appendDuration(b, *(*time.Duration)(p))
344 | // b = append(b, '"')
345 | // return b, nil
346 | // }
347 | //
348 | // b = append(b, e.clrs.Time...)
349 | // b = append(b, '"')
350 | // b = appendDuration(b, *(*time.Duration)(p))
351 | // b = append(b, '"')
352 | // b = append(b, ansiReset...)
353 | // return b, nil
354 | }
355 |
356 | func (e encoder) encodeTime(b []byte, p unsafe.Pointer) ([]byte, error) {
357 | if e.clrs == nil {
358 | t := *(*time.Time)(p)
359 | b = append(b, '"')
360 | b = t.AppendFormat(b, time.RFC3339Nano)
361 | b = append(b, '"')
362 | return b, nil
363 | }
364 |
365 | t := *(*time.Time)(p)
366 | b = append(b, e.clrs.Time...)
367 | b = append(b, '"')
368 | b = t.AppendFormat(b, time.RFC3339Nano)
369 | b = append(b, '"')
370 | b = append(b, ansiReset...)
371 | return b, nil
372 | }
373 |
374 | func (e encoder) encodeArray(b []byte, p unsafe.Pointer, n int, size uintptr, _ reflect.Type, encode encodeFunc) ([]byte, error) {
375 | start := len(b)
376 | var err error
377 |
378 | b = e.clrs.appendPunc(b, '[')
379 |
380 | if n > 0 {
381 | e.indentr.push()
382 | for i := 0; i < n; i++ {
383 | if i != 0 {
384 | b = e.clrs.appendPunc(b, ',')
385 | }
386 |
387 | b = e.indentr.appendByte(b, '\n')
388 | b = e.indentr.appendIndent(b)
389 |
390 | if b, err = encode(e, b, unsafe.Pointer(uintptr(p)+(uintptr(i)*size))); err != nil {
391 | return b[:start], err
392 | }
393 | }
394 | e.indentr.pop()
395 | b = e.indentr.appendByte(b, '\n')
396 | b = e.indentr.appendIndent(b)
397 | }
398 |
399 | b = e.clrs.appendPunc(b, ']')
400 |
401 | return b, nil
402 | }
403 |
404 | func (e encoder) encodeSlice(b []byte, p unsafe.Pointer, size uintptr, t reflect.Type, encode encodeFunc) ([]byte, error) {
405 | s := (*slice)(p)
406 |
407 | if s.data == nil && s.len == 0 && s.cap == 0 {
408 | return e.clrs.appendNull(b), nil
409 | }
410 |
411 | return e.encodeArray(b, s.data, s.len, size, t, encode)
412 | }
413 |
414 | func (e encoder) encodeMap(b []byte, p unsafe.Pointer, t reflect.Type, encodeKey, encodeValue encodeFunc, sortKeys sortFunc) ([]byte, error) {
415 | m := reflect.NewAt(t, p).Elem()
416 | if m.IsNil() {
417 | return e.clrs.appendNull(b), nil
418 | }
419 |
420 | keys := m.MapKeys()
421 | if sortKeys != nil && (e.flags&SortMapKeys) != 0 {
422 | sortKeys(keys)
423 | }
424 |
425 | start := len(b)
426 | var err error
427 | b = e.clrs.appendPunc(b, '{')
428 |
429 | if len(keys) != 0 {
430 | b = e.indentr.appendByte(b, '\n')
431 |
432 | e.indentr.push()
433 | for i := range keys {
434 | k := keys[i]
435 | v := m.MapIndex(k)
436 |
437 | if i != 0 {
438 | b = e.clrs.appendPunc(b, ',')
439 | b = e.indentr.appendByte(b, '\n')
440 | }
441 |
442 | b = e.indentr.appendIndent(b)
443 | if b, err = encodeKey(e, b, (*iface)(unsafe.Pointer(&k)).ptr); err != nil {
444 | return b[:start], err
445 | }
446 |
447 | b = e.clrs.appendPunc(b, ':')
448 | b = e.indentr.appendByte(b, ' ')
449 |
450 | if b, err = encodeValue(e, b, (*iface)(unsafe.Pointer(&v)).ptr); err != nil {
451 | return b[:start], err
452 | }
453 | }
454 | b = e.indentr.appendByte(b, '\n')
455 | e.indentr.pop()
456 | b = e.indentr.appendIndent(b)
457 | }
458 |
459 | b = e.clrs.appendPunc(b, '}')
460 | return b, nil
461 | }
462 |
463 | type element struct {
464 | key string
465 | val interface{}
466 | raw RawMessage
467 | }
468 |
469 | type mapslice struct {
470 | elements []element
471 | }
472 |
473 | func (m *mapslice) Len() int { return len(m.elements) }
474 | func (m *mapslice) Less(i, j int) bool { return m.elements[i].key < m.elements[j].key }
475 | func (m *mapslice) Swap(i, j int) { m.elements[i], m.elements[j] = m.elements[j], m.elements[i] }
476 |
477 | var mapslicePool = sync.Pool{
478 | New: func() interface{} { return new(mapslice) },
479 | }
480 |
481 | func (e encoder) encodeMapStringInterface(b []byte, p unsafe.Pointer) ([]byte, error) {
482 | m := *(*map[string]interface{})(p)
483 | if m == nil {
484 | return e.clrs.appendNull(b), nil
485 | }
486 |
487 | if (e.flags & SortMapKeys) == 0 {
488 | // Optimized code path when the program does not need the map keys to be
489 | // sorted.
490 | b = e.clrs.appendPunc(b, '{')
491 |
492 | if len(m) != 0 {
493 | b = e.indentr.appendByte(b, '\n')
494 |
495 | var err error
496 | i := 0
497 |
498 | e.indentr.push()
499 | for k, v := range m {
500 | if i != 0 {
501 | b = e.clrs.appendPunc(b, ',')
502 | b = e.indentr.appendByte(b, '\n')
503 | }
504 |
505 | b = e.indentr.appendIndent(b)
506 |
507 | b, err = e.encodeKey(b, unsafe.Pointer(&k))
508 | if err != nil {
509 | return b, err
510 | }
511 |
512 | b = e.clrs.appendPunc(b, ':')
513 | b = e.indentr.appendByte(b, ' ')
514 |
515 | b, err = Append(b, v, e.flags, e.clrs, e.indentr)
516 | if err != nil {
517 | return b, err
518 | }
519 |
520 | i++
521 | }
522 | b = e.indentr.appendByte(b, '\n')
523 | e.indentr.pop()
524 | b = e.indentr.appendIndent(b)
525 | }
526 |
527 | b = e.clrs.appendPunc(b, '}')
528 | return b, nil
529 | }
530 |
531 | s := mapslicePool.Get().(*mapslice) //nolint:errcheck
532 | if cap(s.elements) < len(m) {
533 | s.elements = make([]element, 0, align(10, uintptr(len(m))))
534 | }
535 | for key, val := range m {
536 | s.elements = append(s.elements, element{key: key, val: val})
537 | }
538 | sort.Sort(s)
539 |
540 | start := len(b)
541 | var err error
542 | b = e.clrs.appendPunc(b, '{')
543 |
544 | if len(s.elements) > 0 {
545 | b = e.indentr.appendByte(b, '\n')
546 |
547 | e.indentr.push()
548 | for i := range s.elements {
549 | elem := s.elements[i]
550 | if i != 0 {
551 | b = e.clrs.appendPunc(b, ',')
552 | b = e.indentr.appendByte(b, '\n')
553 | }
554 |
555 | b = e.indentr.appendIndent(b)
556 |
557 | b, _ = e.encodeKey(b, unsafe.Pointer(&elem.key))
558 | b = e.clrs.appendPunc(b, ':')
559 | b = e.indentr.appendByte(b, ' ')
560 |
561 | b, err = Append(b, elem.val, e.flags, e.clrs, e.indentr)
562 | if err != nil {
563 | break
564 | }
565 | }
566 | b = e.indentr.appendByte(b, '\n')
567 | e.indentr.pop()
568 | b = e.indentr.appendIndent(b)
569 | }
570 |
571 | for i := range s.elements {
572 | s.elements[i] = element{}
573 | }
574 |
575 | s.elements = s.elements[:0]
576 | mapslicePool.Put(s)
577 |
578 | if err != nil {
579 | return b[:start], err
580 | }
581 |
582 | b = e.clrs.appendPunc(b, '}')
583 | return b, nil
584 | }
585 |
586 | func (e encoder) encodeMapStringRawMessage(b []byte, p unsafe.Pointer) ([]byte, error) {
587 | m := *(*map[string]RawMessage)(p)
588 | if m == nil {
589 | return e.clrs.appendNull(b), nil
590 | }
591 |
592 | if (e.flags & SortMapKeys) == 0 {
593 | // Optimized code path when the program does not need the map keys to be
594 | // sorted.
595 | b = e.clrs.appendPunc(b, '{')
596 |
597 | if len(m) != 0 {
598 | b = e.indentr.appendByte(b, '\n')
599 |
600 | var err error
601 | i := 0
602 |
603 | e.indentr.push()
604 | for k := range m {
605 | if i != 0 {
606 | b = e.clrs.appendPunc(b, ',')
607 | b = e.indentr.appendByte(b, '\n')
608 | }
609 |
610 | b = e.indentr.appendIndent(b)
611 |
612 | b, _ = e.encodeKey(b, unsafe.Pointer(&k))
613 |
614 | b = e.clrs.appendPunc(b, ':')
615 | b = e.indentr.appendByte(b, ' ')
616 |
617 | v := m[k]
618 | b, err = e.encodeRawMessage(b, unsafe.Pointer(&v))
619 | if err != nil {
620 | break
621 | }
622 |
623 | i++
624 | }
625 | b = e.indentr.appendByte(b, '\n')
626 | e.indentr.pop()
627 | b = e.indentr.appendIndent(b)
628 | }
629 |
630 | b = e.clrs.appendPunc(b, '}')
631 | return b, nil
632 | }
633 |
634 | s := mapslicePool.Get().(*mapslice) //nolint:errcheck
635 | if cap(s.elements) < len(m) {
636 | s.elements = make([]element, 0, align(10, uintptr(len(m))))
637 | }
638 | for key, raw := range m {
639 | s.elements = append(s.elements, element{key: key, raw: raw})
640 | }
641 | sort.Sort(s)
642 |
643 | start := len(b)
644 | var err error
645 | b = e.clrs.appendPunc(b, '{')
646 |
647 | if len(s.elements) > 0 {
648 | b = e.indentr.appendByte(b, '\n')
649 |
650 | e.indentr.push()
651 |
652 | for i := range s.elements {
653 | if i != 0 {
654 | b = e.clrs.appendPunc(b, ',')
655 | b = e.indentr.appendByte(b, '\n')
656 | }
657 |
658 | b = e.indentr.appendIndent(b)
659 |
660 | elem := s.elements[i]
661 | b, _ = e.encodeKey(b, unsafe.Pointer(&elem.key))
662 | b = e.clrs.appendPunc(b, ':')
663 | b = e.indentr.appendByte(b, ' ')
664 |
665 | b, err = e.encodeRawMessage(b, unsafe.Pointer(&elem.raw))
666 | if err != nil {
667 | break
668 | }
669 | }
670 | b = e.indentr.appendByte(b, '\n')
671 | e.indentr.pop()
672 | b = e.indentr.appendIndent(b)
673 | }
674 |
675 | for i := range s.elements {
676 | s.elements[i] = element{}
677 | }
678 |
679 | s.elements = s.elements[:0]
680 | mapslicePool.Put(s)
681 |
682 | if err != nil {
683 | return b[:start], err
684 | }
685 |
686 | b = e.clrs.appendPunc(b, '}')
687 | return b, nil
688 | }
689 |
690 | func (e encoder) encodeStruct(b []byte, p unsafe.Pointer, st *structType) ([]byte, error) {
691 | var err error
692 | var k string
693 | var n int
694 | start := len(b)
695 |
696 | b = e.clrs.appendPunc(b, '{')
697 |
698 | if len(st.fields) > 0 {
699 | b = e.indentr.appendByte(b, '\n')
700 | }
701 |
702 | e.indentr.push()
703 |
704 | for i := range st.fields {
705 | f := &st.fields[i]
706 | v := unsafe.Pointer(uintptr(p) + f.offset)
707 |
708 | if f.omitempty && f.empty(v) {
709 | continue
710 | }
711 |
712 | if n != 0 {
713 | b = e.clrs.appendPunc(b, ',')
714 | b = e.indentr.appendByte(b, '\n')
715 | }
716 |
717 | if (e.flags & EscapeHTML) != 0 {
718 | k = f.html
719 | } else {
720 | k = f.json
721 | }
722 |
723 | lengthBeforeKey := len(b)
724 | b = e.indentr.appendIndent(b)
725 |
726 | if e.clrs == nil {
727 | b = append(b, k...)
728 | } else {
729 | b = append(b, e.clrs.Key...)
730 | b = append(b, k...)
731 | b = append(b, ansiReset...)
732 | }
733 |
734 | b = e.clrs.appendPunc(b, ':')
735 |
736 | b = e.indentr.appendByte(b, ' ')
737 |
738 | if b, err = f.codec.encode(e, b, v); err != nil {
739 | if errors.Is(err, rollback{}) {
740 | b = b[:lengthBeforeKey]
741 | continue
742 | }
743 | return b[:start], err
744 | }
745 |
746 | n++
747 | }
748 |
749 | if n > 0 {
750 | b = e.indentr.appendByte(b, '\n')
751 | }
752 |
753 | e.indentr.pop()
754 | b = e.indentr.appendIndent(b)
755 |
756 | b = e.clrs.appendPunc(b, '}')
757 | return b, nil
758 | }
759 |
760 | type rollback struct{}
761 |
762 | func (rollback) Error() string { return "rollback" }
763 |
764 | func (e encoder) encodeEmbeddedStructPointer(b []byte, p unsafe.Pointer, _ reflect.Type, _ bool, offset uintptr, encode encodeFunc) ([]byte, error) {
765 | p = *(*unsafe.Pointer)(p)
766 | if p == nil {
767 | return b, rollback{}
768 | }
769 | return encode(e, b, unsafe.Pointer(uintptr(p)+offset))
770 | }
771 |
772 | func (e encoder) encodePointer(b []byte, p unsafe.Pointer, _ reflect.Type, encode encodeFunc) ([]byte, error) {
773 | if p = *(*unsafe.Pointer)(p); p != nil {
774 | return encode(e, b, p)
775 | }
776 | return e.encodeNull(b, nil)
777 | }
778 |
779 | func (e encoder) encodeInterface(b []byte, p unsafe.Pointer) ([]byte, error) {
780 | return Append(b, *(*interface{})(p), e.flags, e.clrs, e.indentr)
781 | }
782 |
783 | func (e encoder) encodeMaybeEmptyInterface(b []byte, p unsafe.Pointer, t reflect.Type) ([]byte, error) {
784 | return Append(b, reflect.NewAt(t, p).Elem().Interface(), e.flags, e.clrs, e.indentr)
785 | }
786 |
787 | func (e encoder) encodeUnsupportedTypeError(b []byte, _ unsafe.Pointer, t reflect.Type) ([]byte, error) {
788 | return b, &UnsupportedTypeError{Type: t}
789 | }
790 |
791 | // encodeRawMessage encodes a RawMessage to bytes. Unfortunately, this
792 | // implementation has a deficiency: it uses Unmarshal to build an
793 | // object from the RawMessage, which in the case of a struct, results
794 | // in a map being constructed, and thus the order of the keys is not
795 | // guaranteed to be maintained. A superior implementation would decode and
796 | // then re-encode (with color/indentation) the basic JSON tokens on the fly.
797 | // Note also that if TrustRawMessage is set, and the RawMessage is
798 | // invalid JSON (cannot be parsed by Unmarshal), then this function
799 | // falls back to encodeRawMessageNoParseTrusted, which seems to exhibit the
800 | // correct behavior. It's a bit of a mess, but seems to do the trick.
801 | func (e encoder) encodeRawMessage(b []byte, p unsafe.Pointer) ([]byte, error) {
802 | v := *(*RawMessage)(p)
803 |
804 | if v == nil {
805 | return e.clrs.appendNull(b), nil
806 | }
807 |
808 | var s []byte
809 |
810 | if (e.flags & TrustRawMessage) != 0 {
811 | s = v
812 | } else {
813 | var err error
814 | s, _, err = parseValue(v)
815 | if err != nil {
816 | return b, &UnsupportedValueError{Value: reflect.ValueOf(v), Str: err.Error()}
817 | }
818 | }
819 |
820 | var x interface{}
821 | if err := Unmarshal(s, &x); err != nil {
822 | return e.encodeRawMessageNoParseTrusted(b, p)
823 | }
824 |
825 | return Append(b, x, e.flags, e.clrs, e.indentr)
826 | }
827 |
828 | // encodeRawMessageNoParseTrusted is a fallback method that is
829 | // used by encodeRawMessage if it fails to parse a trusted RawMessage.
830 | // The (invalid) JSON produced by this method is not colorized.
831 | // This method may have wonky logic or even bugs in it; little effort
832 | // has been expended on it because it's a rarely visited edge case.
833 | func (e encoder) encodeRawMessageNoParseTrusted(b []byte, p unsafe.Pointer) ([]byte, error) {
834 | v := *(*RawMessage)(p)
835 |
836 | if v == nil {
837 | return e.clrs.appendNull(b), nil
838 | }
839 |
840 | var s []byte
841 |
842 | if (e.flags & TrustRawMessage) != 0 {
843 | s = v
844 | } else {
845 | var err error
846 | s, _, err = parseValue(v)
847 | if err != nil {
848 | return b, &UnsupportedValueError{Value: reflect.ValueOf(v), Str: err.Error()}
849 | }
850 | }
851 |
852 | if e.indentr == nil {
853 | if (e.flags & EscapeHTML) != 0 {
854 | return appendCompactEscapeHTML(b, s), nil
855 | }
856 |
857 | return append(b, s...), nil
858 | }
859 |
860 | // In order to get the tests inherited from the original segmentio
861 | // encoder to work, we need to support indentation.
862 |
863 | // This below is sloppy, but seems to work.
864 | if (e.flags & EscapeHTML) != 0 {
865 | s = appendCompactEscapeHTML(nil, s)
866 | }
867 |
868 | // The "prefix" arg to Indent is the current indentation.
869 | pre := e.indentr.appendIndent(nil)
870 |
871 | buf := &bytes.Buffer{}
872 | // And now we just make use of the existing Indent function.
873 | err := Indent(buf, s, string(pre), e.indentr.indent)
874 | if err != nil {
875 | return b, err
876 | }
877 |
878 | s = buf.Bytes()
879 |
880 | return append(b, s...), nil
881 | }
882 |
883 | // encodeJSONMarshaler suffers from the same defect as encodeRawMessage; it
884 | // can result in keys being reordered.
885 | func (e encoder) encodeJSONMarshaler(b []byte, p unsafe.Pointer, t reflect.Type, pointer bool) ([]byte, error) {
886 | v := reflect.NewAt(t, p)
887 |
888 | if !pointer {
889 | v = v.Elem()
890 | }
891 |
892 | switch v.Kind() {
893 | case reflect.Ptr, reflect.Interface:
894 | if v.IsNil() {
895 | return e.clrs.appendNull(b), nil
896 | }
897 | }
898 |
899 | j, err := v.Interface().(Marshaler).MarshalJSON()
900 | if err != nil {
901 | return b, err
902 | }
903 |
904 | // We effectively delegate to the encodeRawMessage method.
905 | return Append(b, RawMessage(j), e.flags, e.clrs, e.indentr)
906 | }
907 |
908 | func (e encoder) encodeTextMarshaler(b []byte, p unsafe.Pointer, t reflect.Type, pointer bool) ([]byte, error) {
909 | v := reflect.NewAt(t, p)
910 |
911 | if !pointer {
912 | v = v.Elem()
913 | }
914 |
915 | switch v.Kind() {
916 | case reflect.Ptr, reflect.Interface:
917 | if v.IsNil() {
918 | return e.clrs.appendNull(b), nil
919 | }
920 | }
921 |
922 | s, err := v.Interface().(encoding.TextMarshaler).MarshalText()
923 | if err != nil {
924 | return b, err
925 | }
926 |
927 | if e.clrs == nil {
928 | return e.doEncodeString(b, unsafe.Pointer(&s))
929 | }
930 |
931 | b = append(b, e.clrs.TextMarshaler...)
932 | b, err = e.doEncodeString(b, unsafe.Pointer(&s))
933 | b = append(b, ansiReset...)
934 | return b, err
935 | }
936 |
937 | func appendCompactEscapeHTML(dst, src []byte) []byte {
938 | start := 0
939 | escape := false
940 | inString := false
941 |
942 | for i, c := range src {
943 | if !inString {
944 | switch c {
945 | case '"': // enter string
946 | inString = true
947 | case ' ', '\n', '\r', '\t': // skip space
948 | if start < i {
949 | dst = append(dst, src[start:i]...)
950 | }
951 | start = i + 1
952 | }
953 | continue
954 | }
955 |
956 | if escape {
957 | escape = false
958 | continue
959 | }
960 |
961 | if c == '\\' {
962 | escape = true
963 | continue
964 | }
965 |
966 | if c == '"' {
967 | inString = false
968 | continue
969 | }
970 |
971 | if c == '<' || c == '>' || c == '&' {
972 | if start < i {
973 | dst = append(dst, src[start:i]...)
974 | }
975 | dst = append(dst, `\u00`...)
976 | dst = append(dst, hex[c>>4], hex[c&0xF])
977 | start = i + 1
978 | continue
979 | }
980 |
981 | // Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9).
982 | if c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 {
983 | if start < i {
984 | dst = append(dst, src[start:i]...)
985 | }
986 | dst = append(dst, `\u202`...)
987 | dst = append(dst, hex[src[i+2]&0xF])
988 | start = i + 3
989 | continue
990 | }
991 | }
992 |
993 | if start < len(src) {
994 | dst = append(dst, src[start:]...)
995 | }
996 |
997 | return dst
998 | }
999 |
1000 | // indenter is used to indent JSON. The push and pop methods
1001 | // change indentation level. The appendIndent method appends the
1002 | // computed indentation. The appendByte method appends a byte. All
1003 | // methods are safe to use with a nil receiver.
1004 | type indenter struct {
1005 | disabled bool
1006 | prefix string
1007 | indent string
1008 | depth int
1009 | }
1010 |
1011 | // newIndenter returns a new indenter instance. If prefix and
1012 | // indent are both empty, the indenter is effectively disabled,
1013 | // and the appendIndent and appendByte methods are no-op.
1014 | func newIndenter(prefix, indent string) *indenter {
1015 | return &indenter{
1016 | disabled: prefix == "" && indent == "",
1017 | prefix: prefix,
1018 | indent: indent,
1019 | }
1020 | }
1021 |
1022 | // push increases the indentation level.
1023 | func (in *indenter) push() {
1024 | if in != nil {
1025 | in.depth++
1026 | }
1027 | }
1028 |
1029 | // pop decreases the indentation level.
1030 | func (in *indenter) pop() {
1031 | if in != nil {
1032 | in.depth--
1033 | }
1034 | }
1035 |
1036 | // appendByte appends a to b if the indenter is non-nil and enabled.
1037 | // Otherwise b is returned unmodified.
1038 | func (in *indenter) appendByte(b []byte, a byte) []byte {
1039 | if in == nil || in.disabled {
1040 | return b
1041 | }
1042 |
1043 | return append(b, a)
1044 | }
1045 |
1046 | // appendIndent writes indentation to b, returning the resulting slice.
1047 | // If the indenter is nil or disabled b is returned unchanged.
1048 | func (in *indenter) appendIndent(b []byte) []byte {
1049 | if in == nil || in.disabled {
1050 | return b
1051 | }
1052 |
1053 | b = append(b, in.prefix...)
1054 | for i := 0; i < in.depth; i++ {
1055 | b = append(b, in.indent...)
1056 | }
1057 | return b
1058 | }
1059 |
--------------------------------------------------------------------------------
/example_test.go:
--------------------------------------------------------------------------------
1 | package jsoncolor_test
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/fatih/color"
8 | "github.com/neilotoole/jsoncolor/helper/fatihcolor"
9 |
10 | "github.com/mattn/go-colorable"
11 | json "github.com/neilotoole/jsoncolor"
12 | )
13 |
14 | // ExampleEncoder shows use of neilotoole/jsoncolor Encoder.
15 | func ExampleEncoder() {
16 | var enc *json.Encoder
17 |
18 | // Note: this check will fail if running inside Goland (and
19 | // other IDEs?) as IsColorTerminal will return false.
20 | if json.IsColorTerminal(os.Stdout) {
21 | // Safe to use color
22 | out := colorable.NewColorable(os.Stdout) // needed for Windows
23 | enc = json.NewEncoder(out)
24 |
25 | // DefaultColors are similar to jq
26 | clrs := json.DefaultColors()
27 |
28 | // Change some values, just for fun
29 | clrs.Bool = json.Color("\x1b[36m") // Change the bool color
30 | clrs.String = json.Color{} // Disable the string color
31 |
32 | enc.SetColors(clrs)
33 | } else {
34 | // Can't use color; but the encoder will still work
35 | enc = json.NewEncoder(os.Stdout)
36 | }
37 |
38 | m := map[string]interface{}{
39 | "a": 1,
40 | "b": true,
41 | "c": "hello",
42 | }
43 |
44 | if err := enc.Encode(m); err != nil {
45 | fmt.Fprintln(os.Stderr, err)
46 | }
47 | }
48 |
49 | // ExampleFatihColor shows use of the fatihcolor helper package
50 | // with jsoncolor.
51 | func ExampleFatihColor() {
52 | var enc *json.Encoder
53 |
54 | // Note: this check will fail if running inside Goland (and
55 | // other IDEs?) as IsColorTerminal will return false.
56 | if json.IsColorTerminal(os.Stdout) {
57 | out := colorable.NewColorable(os.Stdout)
58 | enc = json.NewEncoder(out)
59 |
60 | fclrs := fatihcolor.DefaultColors()
61 |
62 | // Change some values, just for fun
63 | fclrs.Number = color.New(color.FgBlue)
64 | fclrs.String = color.New(color.FgCyan)
65 |
66 | clrs := fatihcolor.ToCoreColors(fclrs)
67 | enc.SetColors(clrs)
68 | } else {
69 | enc = json.NewEncoder(os.Stdout)
70 | }
71 | enc.SetIndent("", " ")
72 |
73 | m := map[string]interface{}{
74 | "a": 1,
75 | "b": true,
76 | "c": "hello",
77 | }
78 |
79 | if err := enc.Encode(m); err != nil {
80 | fmt.Fprintln(os.Stderr, err)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/neilotoole/jsoncolor
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/fatih/color v1.16.0
7 | github.com/mattn/go-colorable v0.1.13
8 | golang.org/x/sys v0.14.0
9 | golang.org/x/term v0.14.0
10 | )
11 |
12 | require (
13 | // Only used for test/benchmark/comparison.
14 | github.com/nwidger/jsoncolor v0.3.2
15 | // Only used for test/benchmark/comparison.
16 | github.com/segmentio/encoding v0.3.6
17 | github.com/stretchr/testify v1.8.4
18 | )
19 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
5 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
6 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
7 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
8 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
9 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
10 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
11 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
12 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
13 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
14 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
15 | github.com/nwidger/jsoncolor v0.3.2 h1:rVJJlwAWDJShnbTYOQ5RM7yTA20INyKXlJ/fg4JMhHQ=
16 | github.com/nwidger/jsoncolor v0.3.2/go.mod h1:Cs34umxLbJvgBMnVNVqhji9BhoT/N/KinHqZptQ7cf4=
17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
19 | github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
20 | github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
21 | github.com/segmentio/encoding v0.3.6 h1:E6lVLyDPseWEulBmCmAKPanDd3jiyGDo5gMcugCRwZQ=
22 | github.com/segmentio/encoding v0.3.6/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM=
23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
24 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
25 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
26 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
27 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
28 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
29 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
30 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
31 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
32 | golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
35 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
36 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
37 | golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
38 | golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
41 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
44 |
--------------------------------------------------------------------------------
/golang_bench_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2011 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // Large data benchmark.
6 | // The JSON data is a summary of agl's changes in the
7 | // go, webkit, and chromium open source projects.
8 | // We benchmark converting between the JSON form
9 | // and in-memory data structures.
10 |
11 | package jsoncolor
12 |
13 | import (
14 | "bytes"
15 | "compress/gzip"
16 | "fmt"
17 | "io/ioutil"
18 | "os"
19 | "reflect"
20 | "runtime"
21 | "strings"
22 | "sync"
23 | "testing"
24 | )
25 |
26 | type codeResponse struct {
27 | Tree *codeNode `json:"tree"`
28 | Username string `json:"username"`
29 | }
30 |
31 | type codeNode struct {
32 | Name string `json:"name"`
33 | Kids []*codeNode `json:"kids"`
34 | CLWeight float64 `json:"cl_weight"`
35 | Touches int `json:"touches"`
36 | MinT int64 `json:"min_t"`
37 | MaxT int64 `json:"max_t"`
38 | MeanT int64 `json:"mean_t"`
39 | }
40 |
41 | var (
42 | codeJSON []byte
43 | codeStruct codeResponse
44 | )
45 |
46 | func codeInit() {
47 | f, err := os.Open("testdata/code.json.gz")
48 | if err != nil {
49 | panic(err)
50 | }
51 | defer f.Close()
52 | gz, err := gzip.NewReader(f)
53 | if err != nil {
54 | panic(err)
55 | }
56 | data, err := ioutil.ReadAll(gz)
57 | if err != nil {
58 | panic(err)
59 | }
60 |
61 | codeJSON = data
62 |
63 | if err := Unmarshal(codeJSON, &codeStruct); err != nil {
64 | panic("unmarshal code.json: " + err.Error())
65 | }
66 |
67 | if data, err = Marshal(&codeStruct); err != nil {
68 | panic("marshal code.json: " + err.Error())
69 | }
70 |
71 | if !bytes.Equal(data, codeJSON) {
72 | println("different lengths", len(data), len(codeJSON))
73 | for i := 0; i < len(data) && i < len(codeJSON); i++ {
74 | if data[i] != codeJSON[i] {
75 | println("re-marshal: changed at byte", i)
76 | println("orig: ", string(codeJSON[i-10:i+10]))
77 | println("new: ", string(data[i-10:i+10]))
78 | break
79 | }
80 | }
81 | panic("re-marshal code.json: different result")
82 | }
83 | }
84 |
85 | func BenchmarkCodeEncoder(b *testing.B) {
86 | b.ReportAllocs()
87 | if codeJSON == nil {
88 | b.StopTimer()
89 | codeInit()
90 | b.StartTimer()
91 | }
92 | b.RunParallel(func(pb *testing.PB) {
93 | enc := NewEncoder(ioutil.Discard)
94 | for pb.Next() {
95 | if err := enc.Encode(&codeStruct); err != nil {
96 | b.Fatal("Encode:", err)
97 | }
98 | }
99 | })
100 | b.SetBytes(int64(len(codeJSON)))
101 | }
102 |
103 | func BenchmarkCodeMarshal(b *testing.B) {
104 | b.ReportAllocs()
105 | if codeJSON == nil {
106 | b.StopTimer()
107 | codeInit()
108 | b.StartTimer()
109 | }
110 | b.RunParallel(func(pb *testing.PB) {
111 | for pb.Next() {
112 | if _, err := Marshal(&codeStruct); err != nil {
113 | b.Fatal("Marshal:", err)
114 | }
115 | }
116 | })
117 | b.SetBytes(int64(len(codeJSON)))
118 | }
119 |
120 | func benchMarshalBytes(n int) func(*testing.B) {
121 | sample := []byte("hello world")
122 | // Use a struct pointer, to avoid an allocation when passing it as an
123 | // interface parameter to Marshal.
124 | v := &struct {
125 | Bytes []byte
126 | }{
127 | bytes.Repeat(sample, (n/len(sample))+1)[:n],
128 | }
129 | return func(b *testing.B) {
130 | for i := 0; i < b.N; i++ {
131 | if _, err := Marshal(v); err != nil {
132 | b.Fatal("Marshal:", err)
133 | }
134 | }
135 | }
136 | }
137 |
138 | func BenchmarkMarshalBytes(b *testing.B) {
139 | b.ReportAllocs()
140 | // 32 fits within encodeState.scratch.
141 | b.Run("32", benchMarshalBytes(32))
142 | // 256 doesn't fit in encodeState.scratch, but is small enough to
143 | // allocate and avoid the slower base64.NewEncoder.
144 | b.Run("256", benchMarshalBytes(256))
145 | // 4096 is large enough that we want to avoid allocating for it.
146 | b.Run("4096", benchMarshalBytes(4096))
147 | }
148 |
149 | func BenchmarkCodeDecoder(b *testing.B) {
150 | b.ReportAllocs()
151 | if codeJSON == nil {
152 | b.StopTimer()
153 | codeInit()
154 | b.StartTimer()
155 | }
156 | b.RunParallel(func(pb *testing.PB) {
157 | var buf bytes.Buffer
158 | dec := NewDecoder(&buf)
159 | var r codeResponse
160 | for pb.Next() {
161 | buf.Write(codeJSON)
162 | // hide EOF
163 | buf.WriteByte('\n')
164 | buf.WriteByte('\n')
165 | buf.WriteByte('\n')
166 | if err := dec.Decode(&r); err != nil {
167 | b.Fatal("Decode:", err)
168 | }
169 | }
170 | })
171 | b.SetBytes(int64(len(codeJSON)))
172 | }
173 |
174 | func BenchmarkUnicodeDecoder(b *testing.B) {
175 | b.ReportAllocs()
176 | j := []byte(`"\uD83D\uDE01"`)
177 | b.SetBytes(int64(len(j)))
178 | r := bytes.NewReader(j)
179 | dec := NewDecoder(r)
180 | var out string
181 | b.ResetTimer()
182 | for i := 0; i < b.N; i++ {
183 | if err := dec.Decode(&out); err != nil {
184 | b.Fatal("Decode:", err)
185 | }
186 | r.Seek(0, 0)
187 | }
188 | }
189 |
190 | func BenchmarkDecoderStream(b *testing.B) {
191 | b.ReportAllocs()
192 | b.StopTimer()
193 | var buf bytes.Buffer
194 | dec := NewDecoder(&buf)
195 | buf.WriteString(`"` + strings.Repeat("x", 1000000) + `"` + "\n\n\n")
196 | var x interface{}
197 | if err := dec.Decode(&x); err != nil {
198 | b.Fatal("Decode:", err)
199 | }
200 | ones := strings.Repeat(" 1\n", 300000) + "\n\n\n"
201 | b.StartTimer()
202 | for i := 0; i < b.N; i++ {
203 | if i%300000 == 0 {
204 | buf.WriteString(ones)
205 | }
206 | x = nil
207 | if err := dec.Decode(&x); err != nil || x != 1.0 {
208 | b.Fatalf("Decode: %v after %d", err, i)
209 | }
210 | }
211 | }
212 |
213 | func BenchmarkCodeUnmarshal(b *testing.B) {
214 | b.ReportAllocs()
215 | if codeJSON == nil {
216 | b.StopTimer()
217 | codeInit()
218 | b.StartTimer()
219 | }
220 | b.RunParallel(func(pb *testing.PB) {
221 | for pb.Next() {
222 | var r codeResponse
223 | if err := Unmarshal(codeJSON, &r); err != nil {
224 | b.Fatal("Unmarshal:", err)
225 | }
226 | }
227 | })
228 | b.SetBytes(int64(len(codeJSON)))
229 | }
230 |
231 | func BenchmarkCodeUnmarshalReuse(b *testing.B) {
232 | b.ReportAllocs()
233 | if codeJSON == nil {
234 | b.StopTimer()
235 | codeInit()
236 | b.StartTimer()
237 | }
238 | b.RunParallel(func(pb *testing.PB) {
239 | var r codeResponse
240 | for pb.Next() {
241 | if err := Unmarshal(codeJSON, &r); err != nil {
242 | b.Fatal("Unmarshal:", err)
243 | }
244 | }
245 | })
246 | b.SetBytes(int64(len(codeJSON)))
247 | }
248 |
249 | func BenchmarkUnmarshalString(b *testing.B) {
250 | b.ReportAllocs()
251 | data := []byte(`"hello, world"`)
252 | b.RunParallel(func(pb *testing.PB) {
253 | var s string
254 | for pb.Next() {
255 | if err := Unmarshal(data, &s); err != nil {
256 | b.Fatal("Unmarshal:", err)
257 | }
258 | }
259 | })
260 | }
261 |
262 | func BenchmarkUnmarshalFloat64(b *testing.B) {
263 | b.ReportAllocs()
264 | data := []byte(`3.14`)
265 | b.RunParallel(func(pb *testing.PB) {
266 | var f float64
267 | for pb.Next() {
268 | if err := Unmarshal(data, &f); err != nil {
269 | b.Fatal("Unmarshal:", err)
270 | }
271 | }
272 | })
273 | }
274 |
275 | func BenchmarkUnmarshalInt64(b *testing.B) {
276 | b.ReportAllocs()
277 | data := []byte(`3`)
278 | b.RunParallel(func(pb *testing.PB) {
279 | var x int64
280 | for pb.Next() {
281 | if err := Unmarshal(data, &x); err != nil {
282 | b.Fatal("Unmarshal:", err)
283 | }
284 | }
285 | })
286 | }
287 |
288 | func BenchmarkIssue10335(b *testing.B) {
289 | b.ReportAllocs()
290 | j := []byte(`{"a":{ }}`)
291 | b.RunParallel(func(pb *testing.PB) {
292 | var s struct{}
293 | for pb.Next() {
294 | if err := Unmarshal(j, &s); err != nil {
295 | b.Fatal(err)
296 | }
297 | }
298 | })
299 | }
300 |
301 | func BenchmarkUnmapped(b *testing.B) {
302 | b.ReportAllocs()
303 | j := []byte(`{"s": "hello", "y": 2, "o": {"x": 0}, "a": [1, 99, {"x": 1}]}`)
304 | b.RunParallel(func(pb *testing.PB) {
305 | var s struct{}
306 | for pb.Next() {
307 | if err := Unmarshal(j, &s); err != nil {
308 | b.Fatal(err)
309 | }
310 | }
311 | })
312 | }
313 |
314 | func BenchmarkTypeFieldsCache(b *testing.B) {
315 | b.ReportAllocs()
316 | var maxTypes int = 1e6
317 | if testenv.Builder() != "" {
318 | maxTypes = 1e3 // restrict cache sizes on builders
319 | }
320 |
321 | // Dynamically generate many new types.
322 | types := make([]reflect.Type, maxTypes)
323 | fs := []reflect.StructField{{
324 | Type: reflect.TypeOf(""),
325 | Index: []int{0},
326 | }}
327 | for i := range types {
328 | fs[0].Name = fmt.Sprintf("TypeFieldsCache%d", i)
329 | types[i] = reflect.StructOf(fs)
330 | }
331 |
332 | // clearClear clears the cache. Other JSON operations, must not be running.
333 | clearCache := func() {
334 | fieldCache = sync.Map{}
335 | }
336 |
337 | // MissTypes tests the performance of repeated cache misses.
338 | // This measures the time to rebuild a cache of size nt.
339 | for nt := 1; nt <= maxTypes; nt *= 10 {
340 | ts := types[:nt]
341 | b.Run(fmt.Sprintf("MissTypes%d", nt), func(b *testing.B) {
342 | nc := runtime.GOMAXPROCS(0)
343 | for i := 0; i < b.N; i++ {
344 | clearCache()
345 | var wg sync.WaitGroup
346 | for j := 0; j < nc; j++ {
347 | wg.Add(1)
348 | go func(j int) {
349 | for _, t := range ts[(j*len(ts))/nc : ((j+1)*len(ts))/nc] {
350 | cachedTypeFields(t)
351 | }
352 | wg.Done()
353 | }(j)
354 | }
355 | wg.Wait()
356 | }
357 | })
358 | }
359 |
360 | // HitTypes tests the performance of repeated cache hits.
361 | // This measures the average time of each cache lookup.
362 | for nt := 1; nt <= maxTypes; nt *= 10 {
363 | // Pre-warm a cache of size nt.
364 | clearCache()
365 | for _, t := range types[:nt] {
366 | cachedTypeFields(t)
367 | }
368 | b.Run(fmt.Sprintf("HitTypes%d", nt), func(b *testing.B) {
369 | b.RunParallel(func(pb *testing.PB) {
370 | for pb.Next() {
371 | cachedTypeFields(types[0])
372 | }
373 | })
374 | })
375 | }
376 | }
377 |
--------------------------------------------------------------------------------
/golang_example_marshaling_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package jsoncolor_test
6 |
7 | import (
8 | "encoding/json"
9 | "fmt"
10 | "log"
11 | "strings"
12 | )
13 |
14 | type Animal int
15 |
16 | const (
17 | Unknown Animal = iota
18 | Gopher
19 | Zebra
20 | )
21 |
22 | func (a *Animal) UnmarshalJSON(b []byte) error {
23 | var s string
24 | if err := json.Unmarshal(b, &s); err != nil {
25 | return err
26 | }
27 | switch strings.ToLower(s) {
28 | default:
29 | *a = Unknown
30 | case "gopher":
31 | *a = Gopher
32 | case "zebra":
33 | *a = Zebra
34 | }
35 |
36 | return nil
37 | }
38 |
39 | func (a Animal) MarshalJSON() ([]byte, error) {
40 | var s string
41 | switch a {
42 | default:
43 | s = "unknown"
44 | case Gopher:
45 | s = "gopher"
46 | case Zebra:
47 | s = "zebra"
48 | }
49 |
50 | return json.Marshal(s)
51 | }
52 |
53 | func Example_customMarshalJSON() {
54 | blob := `["gopher","armadillo","zebra","unknown","gopher","bee","gopher","zebra"]`
55 | var zoo []Animal
56 | if err := json.Unmarshal([]byte(blob), &zoo); err != nil {
57 | log.Fatal(err)
58 | }
59 |
60 | census := make(map[Animal]int)
61 | for _, animal := range zoo {
62 | census[animal] += 1
63 | }
64 |
65 | fmt.Printf("Zoo Census:\n* Gophers: %d\n* Zebras: %d\n* Unknown: %d\n",
66 | census[Gopher], census[Zebra], census[Unknown])
67 |
68 | // Output:
69 | // Zoo Census:
70 | // * Gophers: 3
71 | // * Zebras: 2
72 | // * Unknown: 3
73 | }
74 |
--------------------------------------------------------------------------------
/golang_example_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2011 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package jsoncolor_test
6 |
7 | import (
8 | "bytes"
9 | "encoding/json"
10 | "fmt"
11 | "io"
12 | "log"
13 | "os"
14 | "strings"
15 | )
16 |
17 | func ExampleMarshal() {
18 | type ColorGroup struct {
19 | ID int
20 | Name string
21 | Colors []string
22 | }
23 | group := ColorGroup{
24 | ID: 1,
25 | Name: "Reds",
26 | Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
27 | }
28 | b, err := json.Marshal(group)
29 | if err != nil {
30 | fmt.Println("error:", err)
31 | }
32 | os.Stdout.Write(b)
33 | // Output:
34 | // {"ID":1,"Name":"Reds","Colors":["Crimson","Red","Ruby","Maroon"]}
35 | }
36 |
37 | func ExampleUnmarshal() {
38 | jsonBlob := []byte(`[
39 | {"Name": "Platypus", "Order": "Monotremata"},
40 | {"Name": "Quoll", "Order": "Dasyuromorphia"}
41 | ]`)
42 | type Animal struct {
43 | Name string
44 | Order string
45 | }
46 | var animals []Animal
47 | err := json.Unmarshal(jsonBlob, &animals)
48 | if err != nil {
49 | fmt.Println("error:", err)
50 | }
51 | fmt.Printf("%+v", animals)
52 | // Output:
53 | // [{Name:Platypus Order:Monotremata} {Name:Quoll Order:Dasyuromorphia}]
54 | }
55 |
56 | // This example uses a Decoder to decode a stream of distinct JSON values.
57 | func ExampleDecoder() {
58 | const jsonStream = `
59 | {"Name": "Ed", "Text": "Knock knock."}
60 | {"Name": "Sam", "Text": "Who's there?"}
61 | {"Name": "Ed", "Text": "Go fmt."}
62 | {"Name": "Sam", "Text": "Go fmt who?"}
63 | {"Name": "Ed", "Text": "Go fmt yourself!"}
64 | `
65 | type Message struct {
66 | Name, Text string
67 | }
68 | dec := json.NewDecoder(strings.NewReader(jsonStream))
69 | for {
70 | var m Message
71 | if err := dec.Decode(&m); err == io.EOF {
72 | break
73 | } else if err != nil {
74 | log.Fatal(err)
75 | }
76 | fmt.Printf("%s: %s\n", m.Name, m.Text)
77 | }
78 | // Output:
79 | // Ed: Knock knock.
80 | // Sam: Who's there?
81 | // Ed: Go fmt.
82 | // Sam: Go fmt who?
83 | // Ed: Go fmt yourself!
84 | }
85 |
86 | // This example uses a Decoder to decode a stream of distinct JSON values.
87 | func ExampleDecoder_Token() {
88 | const jsonStream = `
89 | {"Message": "Hello", "Array": [1, 2, 3], "Null": null, "Number": 1.234}
90 | `
91 | dec := json.NewDecoder(strings.NewReader(jsonStream))
92 | for {
93 | t, err := dec.Token()
94 | if err == io.EOF {
95 | break
96 | }
97 | if err != nil {
98 | log.Fatal(err)
99 | }
100 | fmt.Printf("%T: %v", t, t)
101 | if dec.More() {
102 | fmt.Printf(" (more)")
103 | }
104 | fmt.Printf("\n")
105 | }
106 | // Output:
107 | // json.Delim: { (more)
108 | // string: Message (more)
109 | // string: Hello (more)
110 | // string: Array (more)
111 | // json.Delim: [ (more)
112 | // float64: 1 (more)
113 | // float64: 2 (more)
114 | // float64: 3
115 | // json.Delim: ] (more)
116 | // string: Null (more)
117 | // : (more)
118 | // string: Number (more)
119 | // float64: 1.234
120 | // json.Delim: }
121 | }
122 |
123 | // This example uses a Decoder to decode a streaming array of JSON objects.
124 | func ExampleDecoder_Decode_stream() {
125 | const jsonStream = `
126 | [
127 | {"Name": "Ed", "Text": "Knock knock."},
128 | {"Name": "Sam", "Text": "Who's there?"},
129 | {"Name": "Ed", "Text": "Go fmt."},
130 | {"Name": "Sam", "Text": "Go fmt who?"},
131 | {"Name": "Ed", "Text": "Go fmt yourself!"}
132 | ]
133 | `
134 | type Message struct {
135 | Name, Text string
136 | }
137 | dec := json.NewDecoder(strings.NewReader(jsonStream))
138 |
139 | // read open bracket
140 | t, err := dec.Token()
141 | if err != nil {
142 | log.Fatal(err)
143 | }
144 | fmt.Printf("%T: %v\n", t, t)
145 |
146 | // while the array contains values
147 | for dec.More() {
148 | var m Message
149 | // decode an array value (Message)
150 | err := dec.Decode(&m)
151 | if err != nil {
152 | log.Fatal(err)
153 | }
154 |
155 | fmt.Printf("%v: %v\n", m.Name, m.Text)
156 | }
157 |
158 | // read closing bracket
159 | t, err = dec.Token()
160 | if err != nil {
161 | log.Fatal(err)
162 | }
163 | fmt.Printf("%T: %v\n", t, t)
164 |
165 | // Output:
166 | // json.Delim: [
167 | // Ed: Knock knock.
168 | // Sam: Who's there?
169 | // Ed: Go fmt.
170 | // Sam: Go fmt who?
171 | // Ed: Go fmt yourself!
172 | // json.Delim: ]
173 | }
174 |
175 | // This example uses RawMessage to delay parsing part of a JSON message.
176 | func ExampleRawMessage_unmarshal() {
177 | type Color struct {
178 | Space string
179 | Point json.RawMessage // delay parsing until we know the color space
180 | }
181 | type RGB struct {
182 | R uint8
183 | G uint8
184 | B uint8
185 | }
186 | type YCbCr struct {
187 | Y uint8
188 | Cb int8
189 | Cr int8
190 | }
191 |
192 | j := []byte(`[
193 | {"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10}},
194 | {"Space": "RGB", "Point": {"R": 98, "G": 218, "B": 255}}
195 | ]`)
196 | var colors []Color
197 | err := json.Unmarshal(j, &colors)
198 | if err != nil {
199 | log.Fatalln("error:", err)
200 | }
201 |
202 | for _, c := range colors {
203 | var dst interface{}
204 | switch c.Space {
205 | case "RGB":
206 | dst = new(RGB)
207 | case "YCbCr":
208 | dst = new(YCbCr)
209 | }
210 | err := json.Unmarshal(c.Point, dst)
211 | if err != nil {
212 | log.Fatalln("error:", err)
213 | }
214 | fmt.Println(c.Space, dst)
215 | }
216 | // Output:
217 | // YCbCr &{255 0 -10}
218 | // RGB &{98 218 255}
219 | }
220 |
221 | // This example uses RawMessage to use a precomputed JSON during marshal.
222 | func ExampleRawMessage_marshal() {
223 | h := json.RawMessage(`{"precomputed": true}`)
224 |
225 | c := struct {
226 | Header *json.RawMessage `json:"header"`
227 | Body string `json:"body"`
228 | }{Header: &h, Body: "Hello Gophers!"}
229 |
230 | b, err := json.MarshalIndent(&c, "", "\t")
231 | if err != nil {
232 | fmt.Println("error:", err)
233 | }
234 | os.Stdout.Write(b)
235 |
236 | // Output:
237 | // {
238 | // "header": {
239 | // "precomputed": true
240 | // },
241 | // "body": "Hello Gophers!"
242 | // }
243 | }
244 |
245 | func ExampleIndent() {
246 | type Road struct {
247 | Name string
248 | Number int
249 | }
250 | roads := []Road{
251 | {"Diamond Fork", 29},
252 | {"Sheep Creek", 51},
253 | }
254 |
255 | b, err := json.Marshal(roads)
256 | if err != nil {
257 | log.Fatal(err)
258 | }
259 |
260 | var out bytes.Buffer
261 | json.Indent(&out, b, "=", "\t")
262 | out.WriteTo(os.Stdout)
263 | // Output:
264 | // [
265 | // = {
266 | // = "Name": "Diamond Fork",
267 | // = "Number": 29
268 | // = },
269 | // = {
270 | // = "Name": "Sheep Creek",
271 | // = "Number": 51
272 | // = }
273 | // =]
274 | }
275 |
276 | func ExampleMarshalIndent() {
277 | data := map[string]int{
278 | "a": 1,
279 | "b": 2,
280 | }
281 |
282 | json, err := json.MarshalIndent(data, "", "")
283 | if err != nil {
284 | log.Fatal(err)
285 | }
286 |
287 | fmt.Println(string(json))
288 | // Output:
289 | // {
290 | // "a": 1,
291 | // "b": 2
292 | // }
293 | }
294 |
295 | func ExampleValid() {
296 | goodJSON := `{"example": 1}`
297 | badJSON := `{"example":2:]}}`
298 |
299 | fmt.Println(json.Valid([]byte(goodJSON)), json.Valid([]byte(badJSON)))
300 | // Output:
301 | // true false
302 | }
303 |
304 | func ExampleHTMLEscape() {
305 | var out bytes.Buffer
306 | json.HTMLEscape(&out, []byte(`{"Name":"HTML content"}`))
307 | out.WriteTo(os.Stdout)
308 | // Output:
309 | //{"Name":"\u003cb\u003eHTML content\u003c/b\u003e"}
310 | }
311 |
--------------------------------------------------------------------------------
/golang_number_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2011 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package jsoncolor
6 |
7 | import (
8 | "regexp"
9 | "testing"
10 | )
11 |
12 | func TestNumberIsValid(t *testing.T) {
13 | // From: https://stackoverflow.com/a/13340826
14 | jsonNumberRegexp := regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`)
15 |
16 | validTests := []string{
17 | "0",
18 | "-0",
19 | "1",
20 | "-1",
21 | "0.1",
22 | "-0.1",
23 | "1234",
24 | "-1234",
25 | "12.34",
26 | "-12.34",
27 | "12E0",
28 | "12E1",
29 | "12e34",
30 | "12E-0",
31 | "12e+1",
32 | "12e-34",
33 | "-12E0",
34 | "-12E1",
35 | "-12e34",
36 | "-12E-0",
37 | "-12e+1",
38 | "-12e-34",
39 | "1.2E0",
40 | "1.2E1",
41 | "1.2e34",
42 | "1.2E-0",
43 | "1.2e+1",
44 | "1.2e-34",
45 | "-1.2E0",
46 | "-1.2E1",
47 | "-1.2e34",
48 | "-1.2E-0",
49 | "-1.2e+1",
50 | "-1.2e-34",
51 | "0E0",
52 | "0E1",
53 | "0e34",
54 | "0E-0",
55 | "0e+1",
56 | "0e-34",
57 | "-0E0",
58 | "-0E1",
59 | "-0e34",
60 | "-0E-0",
61 | "-0e+1",
62 | "-0e-34",
63 | }
64 |
65 | for _, test := range validTests {
66 | if !isValidNumber(test) {
67 | t.Errorf("%s should be valid", test)
68 | }
69 |
70 | var f float64
71 | if err := Unmarshal([]byte(test), &f); err != nil {
72 | t.Errorf("%s should be valid but Unmarshal failed: %v", test, err)
73 | }
74 |
75 | if !jsonNumberRegexp.MatchString(test) {
76 | t.Errorf("%s should be valid but regexp does not match", test)
77 | }
78 | }
79 |
80 | invalidTests := []string{
81 | "",
82 | "invalid",
83 | "1.0.1",
84 | "1..1",
85 | "-1-2",
86 | "012a42",
87 | "01.2",
88 | "012",
89 | "12E12.12",
90 | "1e2e3",
91 | "1e+-2",
92 | "1e--23",
93 | "1e",
94 | "e1",
95 | "1e+",
96 | "1ea",
97 | "1a",
98 | "1.a",
99 | "1.",
100 | "01",
101 | "1.e1",
102 | }
103 |
104 | for _, test := range invalidTests {
105 | var f float64
106 | if err := Unmarshal([]byte(test), &f); err == nil {
107 | t.Errorf("%s should be invalid but unmarshal wrote %v", test, f)
108 | }
109 |
110 | if jsonNumberRegexp.MatchString(test) {
111 | t.Errorf("%s should be invalid but matches regexp", test)
112 | }
113 | }
114 | }
115 |
116 | func BenchmarkNumberIsValid(b *testing.B) {
117 | s := "-61657.61667E+61673"
118 | for i := 0; i < b.N; i++ {
119 | isValidNumber(s)
120 | }
121 | }
122 |
123 | func BenchmarkNumberIsValidRegexp(b *testing.B) {
124 | jsonNumberRegexp := regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`)
125 | s := "-61657.61667E+61673"
126 | for i := 0; i < b.N; i++ {
127 | jsonNumberRegexp.MatchString(s)
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/golang_scanner_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2010 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package jsoncolor
6 |
7 | import (
8 | "bytes"
9 | "math"
10 | "math/rand"
11 | "testing"
12 | )
13 |
14 | var validTests = []struct {
15 | data string
16 | ok bool
17 | }{
18 | {`foo`, false},
19 | {`}{`, false},
20 | {`{]`, false},
21 | {`{}`, true},
22 | {`{"foo":"bar"}`, true},
23 | {`{"foo":"bar","bar":{"baz":["qux"]}}`, true},
24 | }
25 |
26 | func TestValid(t *testing.T) {
27 | for _, tt := range validTests {
28 | if ok := Valid([]byte(tt.data)); ok != tt.ok {
29 | t.Errorf("Valid(%#q) = %v, want %v", tt.data, ok, tt.ok)
30 | }
31 | }
32 | }
33 |
34 | // Tests of simple examples.
35 |
36 | type example struct {
37 | compact string
38 | indent string
39 | }
40 |
41 | var examples = []example{
42 | {`1`, `1`},
43 | {`{}`, `{}`},
44 | {`[]`, `[]`},
45 | {`{"":2}`, "{\n\t\"\": 2\n}"},
46 | {`[3]`, "[\n\t3\n]"},
47 | {`[1,2,3]`, "[\n\t1,\n\t2,\n\t3\n]"},
48 | {`{"x":1}`, "{\n\t\"x\": 1\n}"},
49 | {ex1, ex1i},
50 | {"{\"\":\"<>&\u2028\u2029\"}", "{\n\t\"\": \"<>&\u2028\u2029\"\n}"}, // See golang.org/issue/34070
51 | }
52 |
53 | var ex1 = `[true,false,null,"x",1,1.5,0,-5e+2]`
54 |
55 | var ex1i = `[
56 | true,
57 | false,
58 | null,
59 | "x",
60 | 1,
61 | 1.5,
62 | 0,
63 | -5e+2
64 | ]`
65 |
66 | func TestCompact(t *testing.T) {
67 | var buf bytes.Buffer
68 | for _, tt := range examples {
69 | buf.Reset()
70 | if err := Compact(&buf, []byte(tt.compact)); err != nil {
71 | t.Errorf("Compact(%#q): %v", tt.compact, err)
72 | } else if s := buf.String(); s != tt.compact {
73 | t.Errorf("Compact(%#q) = %#q, want original", tt.compact, s)
74 | }
75 |
76 | buf.Reset()
77 | if err := Compact(&buf, []byte(tt.indent)); err != nil {
78 | t.Errorf("Compact(%#q): %v", tt.indent, err)
79 | continue
80 | } else if s := buf.String(); s != tt.compact {
81 | t.Errorf("Compact(%#q) = %#q, want %#q", tt.indent, s, tt.compact)
82 | }
83 | }
84 | }
85 |
86 | func TestCompactSeparators(t *testing.T) {
87 | // U+2028 and U+2029 should be escaped inside strings.
88 | // They should not appear outside strings.
89 | tests := []struct {
90 | in, compact string
91 | }{
92 | {"{\"\u2028\": 1}", "{\"\u2028\":1}"},
93 | {"{\"\u2029\" :2}", "{\"\u2029\":2}"},
94 | }
95 | for _, tt := range tests {
96 | var buf bytes.Buffer
97 | if err := Compact(&buf, []byte(tt.in)); err != nil {
98 | t.Errorf("Compact(%q): %v", tt.in, err)
99 | } else if s := buf.String(); s != tt.compact {
100 | t.Errorf("Compact(%q) = %q, want %q", tt.in, s, tt.compact)
101 | }
102 | }
103 | }
104 |
105 | func TestIndent(t *testing.T) {
106 | var buf bytes.Buffer
107 | for _, tt := range examples {
108 | buf.Reset()
109 | if err := Indent(&buf, []byte(tt.indent), "", "\t"); err != nil {
110 | t.Errorf("Indent(%#q): %v", tt.indent, err)
111 | } else if s := buf.String(); s != tt.indent {
112 | t.Errorf("Indent(%#q) = %#q, want original", tt.indent, s)
113 | }
114 |
115 | buf.Reset()
116 | if err := Indent(&buf, []byte(tt.compact), "", "\t"); err != nil {
117 | t.Errorf("Indent(%#q): %v", tt.compact, err)
118 | continue
119 | } else if s := buf.String(); s != tt.indent {
120 | t.Errorf("Indent(%#q) = %#q, want %#q", tt.compact, s, tt.indent)
121 | }
122 | }
123 | }
124 |
125 | // Tests of a large random structure.
126 |
127 | func TestCompactBig(t *testing.T) {
128 | initBig()
129 | var buf bytes.Buffer
130 | if err := Compact(&buf, jsonBig); err != nil {
131 | t.Fatalf("Compact: %v", err)
132 | }
133 | b := buf.Bytes()
134 | if !bytes.Equal(b, jsonBig) {
135 | t.Error("Compact(jsonBig) != jsonBig")
136 | diff(t, b, jsonBig)
137 | return
138 | }
139 | }
140 |
141 | func TestIndentBig(t *testing.T) {
142 | t.Parallel()
143 | initBig()
144 | var buf bytes.Buffer
145 | if err := Indent(&buf, jsonBig, "", "\t"); err != nil {
146 | t.Fatalf("Indent1: %v", err)
147 | }
148 | b := buf.Bytes()
149 | if len(b) == len(jsonBig) {
150 | // jsonBig is compact (no unnecessary spaces);
151 | // indenting should make it bigger
152 | t.Fatalf("Indent(jsonBig) did not get bigger")
153 | }
154 |
155 | // should be idempotent
156 | var buf1 bytes.Buffer
157 | if err := Indent(&buf1, b, "", "\t"); err != nil {
158 | t.Fatalf("Indent2: %v", err)
159 | }
160 | b1 := buf1.Bytes()
161 | if !bytes.Equal(b1, b) {
162 | t.Error("Indent(Indent(jsonBig)) != Indent(jsonBig)")
163 | diff(t, b1, b)
164 | return
165 | }
166 |
167 | // should get back to original
168 | buf1.Reset()
169 | if err := Compact(&buf1, b); err != nil {
170 | t.Fatalf("Compact: %v", err)
171 | }
172 | b1 = buf1.Bytes()
173 | if !bytes.Equal(b1, jsonBig) {
174 | t.Error("Compact(Indent(jsonBig)) != jsonBig")
175 | diff(t, b1, jsonBig)
176 | return
177 | }
178 | }
179 |
180 | type indentErrorTest struct {
181 | in string
182 | err error
183 | }
184 |
185 | var indentErrorTests = []indentErrorTest{
186 | {`{"X": "foo", "Y"}`, &testSyntaxError{"invalid character '}' after object key", 17}},
187 | {`{"X": "foo" "Y": "bar"}`, &testSyntaxError{"invalid character '\"' after object key:value pair", 13}},
188 | }
189 |
190 | func TestIndentErrors(t *testing.T) {
191 | for i, tt := range indentErrorTests {
192 | slice := make([]uint8, 0)
193 | buf := bytes.NewBuffer(slice)
194 | err := Indent(buf, []uint8(tt.in), "", "")
195 | assertErrorPresence(t, tt.err, err, i)
196 | }
197 | }
198 |
199 | func diff(t *testing.T, a, b []byte) {
200 | for i := 0; ; i++ {
201 | if i >= len(a) || i >= len(b) || a[i] != b[i] {
202 | j := i - 10
203 | if j < 0 {
204 | j = 0
205 | }
206 | t.Errorf("diverge at %d: «%s» vs «%s»", i, trim(a[j:]), trim(b[j:]))
207 | return
208 | }
209 | }
210 | }
211 |
212 | func trim(b []byte) []byte {
213 | if len(b) > 20 {
214 | return b[0:20]
215 | }
216 | return b
217 | }
218 |
219 | // Generate a random JSON object.
220 |
221 | var jsonBig []byte
222 |
223 | func initBig() {
224 | n := 10000
225 | if testing.Short() {
226 | n = 100
227 | }
228 | b, err := Marshal(genValue(n))
229 | if err != nil {
230 | panic(err)
231 | }
232 | jsonBig = b
233 | }
234 |
235 | func genValue(n int) interface{} {
236 | if n > 1 {
237 | switch rand.Intn(2) {
238 | case 0:
239 | return genArray(n)
240 | case 1:
241 | return genMap(n)
242 | }
243 | }
244 | switch rand.Intn(3) {
245 | case 0:
246 | return rand.Intn(2) == 0
247 | case 1:
248 | return rand.NormFloat64()
249 | case 2:
250 | return genString(30)
251 | }
252 | panic("unreachable")
253 | }
254 |
255 | func genString(stddev float64) string {
256 | n := int(math.Abs(rand.NormFloat64()*stddev + stddev/2))
257 | c := make([]rune, n)
258 | for i := range c {
259 | f := math.Abs(rand.NormFloat64()*64 + 32)
260 | if f > 0x10ffff {
261 | f = 0x10ffff
262 | }
263 | c[i] = rune(f)
264 | }
265 | return string(c)
266 | }
267 |
268 | func genArray(n int) []interface{} {
269 | f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2)))
270 | if f > n {
271 | f = n
272 | }
273 | if f < 1 {
274 | f = 1
275 | }
276 | x := make([]interface{}, f)
277 | for i := range x {
278 | x[i] = genValue(((i+1)*n)/f - (i*n)/f)
279 | }
280 | return x
281 | }
282 |
283 | func genMap(n int) map[string]interface{} {
284 | f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2)))
285 | if f > n {
286 | f = n
287 | }
288 | if n > 0 && f == 0 {
289 | f = 1
290 | }
291 | x := make(map[string]interface{})
292 | for i := 0; i < f; i++ {
293 | x[genString(10)] = genValue(((i+1)*n)/f - (i*n)/f)
294 | }
295 | return x
296 | }
297 |
--------------------------------------------------------------------------------
/golang_shim_test.go:
--------------------------------------------------------------------------------
1 | // This file is a shim for dependencies of golang_*_test.go files that are normally provided by the standard library.
2 | // It helps importing those files with minimal changes.
3 | package jsoncolor
4 |
5 | import (
6 | "bytes"
7 | "reflect"
8 | "sync"
9 | "testing"
10 | )
11 |
12 | // Field cache used in golang_bench_test.go
13 | var fieldCache = sync.Map{}
14 |
15 | func cachedTypeFields(reflect.Type) {}
16 |
17 | // Fake test env for golang_bench_test.go
18 | type testenvShim struct{}
19 |
20 | func (ts testenvShim) Builder() string {
21 | return ""
22 | }
23 |
24 | var testenv testenvShim
25 |
26 | // Fake scanner for golang_decode_test.go
27 | type scanner struct{}
28 |
29 | func checkValid(in []byte, scan *scanner) error {
30 | return nil
31 | }
32 |
33 | // Actual isSpace implementation
34 | func isSpace(c byte) bool {
35 | return c == ' ' || c == '\t' || c == '\r' || c == '\n'
36 | }
37 |
38 | // Fake encoder for golang_encode_test.go
39 | type encodeState struct {
40 | Buffer bytes.Buffer
41 | }
42 |
43 | func (es *encodeState) string(s string, escapeHTML bool) {
44 | }
45 |
46 | func (es *encodeState) stringBytes(b []byte, escapeHTML bool) {
47 | }
48 |
49 | // Fake number test
50 | func isValidNumber(n string) bool {
51 | return true
52 | }
53 |
54 | func assertErrorPresence(t *testing.T, expected, actual error, prefixes ...interface{}) {
55 | if expected != nil && actual == nil {
56 | errorWithPrefixes(t, prefixes, "expected error, but did not get an error")
57 | } else if expected == nil && actual != nil {
58 | errorWithPrefixes(t, prefixes, "did not expect error but got %v", actual)
59 | }
60 | }
61 |
62 | func errorWithPrefixes(t *testing.T, prefixes []interface{}, format string, elements ...interface{}) {
63 | fullFormat := format
64 | allElements := append(prefixes, elements...)
65 |
66 | for range prefixes {
67 | fullFormat = "%v: " + fullFormat
68 | }
69 | t.Errorf(fullFormat, allElements...)
70 | }
71 |
--------------------------------------------------------------------------------
/golang_tagkey_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2011 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package jsoncolor
6 |
7 | import (
8 | "testing"
9 | )
10 |
11 | type basicLatin2xTag struct {
12 | V string `json:"$%-/"`
13 | }
14 |
15 | type basicLatin3xTag struct {
16 | V string `json:"0123456789"`
17 | }
18 |
19 | type basicLatin4xTag struct {
20 | V string `json:"ABCDEFGHIJKLMO"`
21 | }
22 |
23 | type basicLatin5xTag struct {
24 | V string `json:"PQRSTUVWXYZ_"`
25 | }
26 |
27 | type basicLatin6xTag struct {
28 | V string `json:"abcdefghijklmno"`
29 | }
30 |
31 | type basicLatin7xTag struct {
32 | V string `json:"pqrstuvwxyz"`
33 | }
34 |
35 | type miscPlaneTag struct {
36 | V string `json:"色は匂へど"`
37 | }
38 |
39 | type percentSlashTag struct {
40 | V string `json:"text/html%"` // https://golang.org/issue/2718
41 | }
42 |
43 | type punctuationTag struct {
44 | V string `json:"!#$%&()*+-./:<=>?@[]^_{|}~"` // https://golang.org/issue/3546
45 | }
46 |
47 | type dashTag struct {
48 | V string `json:"-,"`
49 | }
50 |
51 | type emptyTag struct {
52 | W string
53 | }
54 |
55 | type misnamedTag struct {
56 | X string `jsom:"Misnamed"`
57 | }
58 |
59 | type badFormatTag struct {
60 | Y string `:"BadFormat"`
61 | }
62 |
63 | type badCodeTag struct {
64 | Z string `json:" !\"#&'()*+,."`
65 | }
66 |
67 | type spaceTag struct {
68 | Q string `json:"With space"`
69 | }
70 |
71 | type unicodeTag struct {
72 | W string `json:"Ελλάδα"`
73 | }
74 |
75 | var structTagObjectKeyTests = []struct {
76 | raw interface{}
77 | value string
78 | key string
79 | }{
80 | {basicLatin2xTag{"2x"}, "2x", "$%-/"},
81 | {basicLatin3xTag{"3x"}, "3x", "0123456789"},
82 | {basicLatin4xTag{"4x"}, "4x", "ABCDEFGHIJKLMO"},
83 | {basicLatin5xTag{"5x"}, "5x", "PQRSTUVWXYZ_"},
84 | {basicLatin6xTag{"6x"}, "6x", "abcdefghijklmno"},
85 | {basicLatin7xTag{"7x"}, "7x", "pqrstuvwxyz"},
86 | {miscPlaneTag{"いろはにほへと"}, "いろはにほへと", "色は匂へど"},
87 | {dashTag{"foo"}, "foo", "-"},
88 | {emptyTag{"Pour Moi"}, "Pour Moi", "W"},
89 | {misnamedTag{"Animal Kingdom"}, "Animal Kingdom", "X"},
90 | {badFormatTag{"Orfevre"}, "Orfevre", "Y"},
91 | {badCodeTag{"Reliable Man"}, "Reliable Man", "Z"},
92 | {percentSlashTag{"brut"}, "brut", "text/html%"},
93 | {punctuationTag{"Union Rags"}, "Union Rags", "!#$%&()*+-./:<=>?@[]^_{|}~"},
94 | {spaceTag{"Perreddu"}, "Perreddu", "With space"},
95 | {unicodeTag{"Loukanikos"}, "Loukanikos", "Ελλάδα"},
96 | }
97 |
98 | func TestStructTagObjectKey(t *testing.T) {
99 | for _, tt := range structTagObjectKeyTests {
100 | b, err := Marshal(tt.raw)
101 | if err != nil {
102 | t.Fatalf("Marshal(%#q) failed: %v", tt.raw, err)
103 | }
104 | var f interface{}
105 | err = Unmarshal(b, &f)
106 | if err != nil {
107 | t.Fatalf("Unmarshal(%#q) failed: %v", b, err)
108 | }
109 | for i, v := range f.(map[string]interface{}) {
110 | switch i {
111 | case tt.key:
112 | if s, ok := v.(string); !ok || s != tt.value {
113 | t.Fatalf("Unexpected value: %#q, want %v", s, tt.value)
114 | }
115 | default:
116 | t.Fatalf("Unexpected key: %#q, from %#q", i, b)
117 | }
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/helper/fatihcolor/fatihcolor.go:
--------------------------------------------------------------------------------
1 | // Package fatihcolor provides an adapter between fatih/color
2 | // and neilotoole/jsoncolor's native mechanism. See ToCoreColors.
3 | package fatihcolor
4 |
5 | import (
6 | "bytes"
7 |
8 | "github.com/fatih/color"
9 | "github.com/neilotoole/jsoncolor"
10 | )
11 |
12 | // Colors encapsulates JSON color output, using fatih/color elements.
13 | // It can be converted to a jsoncolor.Colors using ToCoreColors.
14 | type Colors struct {
15 | // Bool is the color for boolean values.
16 | Bool *color.Color
17 |
18 | // Bytes is the color for byte / binary values.
19 | Bytes *color.Color
20 |
21 | // Datetime is the color for time-related values.
22 | Datetime *color.Color
23 |
24 | // Null is the color for null.
25 | Null *color.Color
26 |
27 | // Number is the color for number values, including int,
28 | // float, decimal etc.
29 | Number *color.Color
30 |
31 | // String is the color for string values.
32 | String *color.Color
33 |
34 | // Key is the color for keys such as a JSON field name.
35 | Key *color.Color
36 |
37 | // Punc is the color for punctuation such as colons, braces, etc.
38 | // Frequently Punc will just be color.Bold.
39 | Punc *color.Color
40 |
41 | // TextMarshaler is the color for types implementing encoding.TextMarshaler.
42 | TextMarshaler *color.Color
43 | }
44 |
45 | // DefaultColors returns default Colors instance.
46 | func DefaultColors() *Colors {
47 | return &Colors{
48 | Bool: color.New(color.FgYellow),
49 | Bytes: color.New(color.Faint),
50 | Datetime: color.New(color.FgGreen, color.Faint),
51 | Key: color.New(color.FgBlue, color.Bold),
52 | Null: color.New(color.Faint),
53 | Number: color.New(color.FgCyan),
54 | String: color.New(color.FgGreen),
55 | Punc: color.New(color.Bold),
56 | TextMarshaler: color.New(color.FgGreen),
57 | }
58 | }
59 |
60 | // ToCoreColors converts clrs to a core jsoncolor.Colors instance.
61 | func ToCoreColors(clrs *Colors) *jsoncolor.Colors {
62 | if clrs == nil {
63 | return nil
64 | }
65 |
66 | return &jsoncolor.Colors{
67 | Null: ToCoreColor(clrs.Null),
68 | Bool: ToCoreColor(clrs.Bool),
69 | Number: ToCoreColor(clrs.Number),
70 | String: ToCoreColor(clrs.String),
71 | Key: ToCoreColor(clrs.Key),
72 | Bytes: ToCoreColor(clrs.Bytes),
73 | Time: ToCoreColor(clrs.Datetime),
74 | Punc: ToCoreColor(clrs.Punc),
75 | TextMarshaler: ToCoreColor(clrs.TextMarshaler),
76 | }
77 | }
78 |
79 | // ToCoreColor creates a jsoncolor.Color instance from a fatih/color
80 | // instance.
81 | func ToCoreColor(c *color.Color) jsoncolor.Color {
82 | if c == nil {
83 | return jsoncolor.Color{}
84 | }
85 |
86 | // Dirty conversion function ahead: print
87 | // a space using c, then grab the bytes printed
88 | // before the space, as those are the bytes we need.
89 | // There's definitely a better way of doing this, but
90 | // it works for now.
91 |
92 | // Make a copy because the pkg-level color.NoColor could be false.
93 | c2 := *c
94 | c2.EnableColor()
95 |
96 | b := []byte(c2.Sprint(" "))
97 | i := bytes.IndexByte(b, ' ')
98 | if i <= 0 {
99 | return jsoncolor.Color{}
100 | }
101 |
102 | return b[:i]
103 | }
104 |
--------------------------------------------------------------------------------
/json.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "io"
8 | "reflect"
9 | "runtime"
10 | "sync"
11 | "unsafe"
12 | )
13 |
14 | // Delim is documented at https://golang.org/pkg/encoding/json/#Delim
15 | type Delim = json.Delim
16 |
17 | // InvalidUnmarshalError is documented at https://golang.org/pkg/encoding/json/#InvalidUnmarshalError
18 | type InvalidUnmarshalError = json.InvalidUnmarshalError
19 |
20 | // Marshaler is documented at https://golang.org/pkg/encoding/json/#Marshaler
21 | type Marshaler = json.Marshaler
22 |
23 | // MarshalerError is documented at https://golang.org/pkg/encoding/json/#MarshalerError
24 | type MarshalerError = json.MarshalerError
25 |
26 | // Number is documented at https://golang.org/pkg/encoding/json/#Number
27 | type Number = json.Number
28 |
29 | // RawMessage is documented at https://golang.org/pkg/encoding/json/#RawMessage
30 | type RawMessage = json.RawMessage
31 |
32 | // A SyntaxError is a description of a JSON syntax error.
33 | type SyntaxError = json.SyntaxError
34 |
35 | // Token is documented at https://golang.org/pkg/encoding/json/#Token
36 | type Token = json.Token
37 |
38 | // UnmarshalTypeError is documented at https://golang.org/pkg/encoding/json/#UnmarshalTypeError
39 | type UnmarshalTypeError = json.UnmarshalTypeError
40 |
41 | // Unmarshaler is documented at https://golang.org/pkg/encoding/json/#Unmarshaler
42 | type Unmarshaler = json.Unmarshaler
43 |
44 | // UnsupportedTypeError is documented at https://golang.org/pkg/encoding/json/#UnsupportedTypeError
45 | type UnsupportedTypeError = json.UnsupportedTypeError
46 |
47 | // UnsupportedValueError is documented at https://golang.org/pkg/encoding/json/#UnsupportedValueError
48 | type UnsupportedValueError = json.UnsupportedValueError
49 |
50 | // AppendFlags is a type used to represent configuration options that can be
51 | // applied when formatting json output.
52 | type AppendFlags int
53 |
54 | const (
55 | // EscapeHTML is a formatting flag used to to escape HTML in json strings.
56 | EscapeHTML AppendFlags = 1 << iota
57 |
58 | // SortMapKeys is formatting flag used to enable sorting of map keys when
59 | // encoding JSON (this matches the behavior of the standard encoding/json
60 | // package).
61 | SortMapKeys
62 |
63 | // TrustRawMessage is a performance optimization flag to skip value
64 | // checking of raw messages. It should only be used if the values are
65 | // known to be valid json (e.g., they were created by json.Unmarshal).
66 | TrustRawMessage
67 | )
68 |
69 | // ParseFlags is a type used to represent configuration options that can be
70 | // applied when parsing json input.
71 | type ParseFlags int
72 |
73 | const (
74 | // DisallowUnknownFields is a parsing flag used to prevent decoding of
75 | // objects to Go struct values when a field of the input does not match
76 | // with any of the struct fields.
77 | DisallowUnknownFields ParseFlags = 1 << iota
78 |
79 | // UseNumber is a parsing flag used to load numeric values as Number
80 | // instead of float64.
81 | UseNumber
82 |
83 | // DontCopyString is a parsing flag used to provide zero-copy support when
84 | // loading string values from a json payload. It is not always possible to
85 | // avoid dynamic memory allocations, for example when a string is escaped in
86 | // the json data a new buffer has to be allocated, but when the `wire` value
87 | // can be used as content of a Go value the decoder will simply point into
88 | // the input buffer.
89 | DontCopyString
90 |
91 | // DontCopyNumber is a parsing flag used to provide zero-copy support when
92 | // loading Number values (see DontCopyString and DontCopyRawMessage).
93 | DontCopyNumber
94 |
95 | // DontCopyRawMessage is a parsing flag used to provide zero-copy support
96 | // when loading RawMessage values from a json payload. When used, the
97 | // RawMessage values will not be allocated into new memory buffers and
98 | // will instead point directly to the area of the input buffer where the
99 | // value was found.
100 | DontCopyRawMessage
101 |
102 | // DontMatchCaseInsensitiveStructFields is a parsing flag used to prevent
103 | // matching fields in a case-insensitive way. This can prevent degrading
104 | // performance on case conversions, and can also act as a stricter decoding
105 | // mode.
106 | DontMatchCaseInsensitiveStructFields
107 |
108 | // ZeroCopy is a parsing flag that combines all the copy optimizations
109 | // available in the package.
110 | //
111 | // The zero-copy optimizations are better used in request-handler style
112 | // code where none of the values are retained after the handler returns.
113 | ZeroCopy = DontCopyString | DontCopyNumber | DontCopyRawMessage
114 | )
115 |
116 | // Append acts like Marshal but appends the json representation to b instead of
117 | // always reallocating a new slice.
118 | func Append(b []byte, x interface{}, flags AppendFlags, clrs *Colors, indentr *indenter) ([]byte, error) {
119 | if x == nil {
120 | // Special case for nil values because it makes the rest of the code
121 | // simpler to assume that it won't be seeing nil pointers.
122 | return clrs.appendNull(b), nil
123 | }
124 |
125 | t := reflect.TypeOf(x)
126 | p := (*iface)(unsafe.Pointer(&x)).ptr
127 |
128 | cache := cacheLoad()
129 | c, found := cache[typeid(t)]
130 |
131 | if !found {
132 | c = constructCachedCodec(t, cache)
133 | }
134 |
135 | b, err := c.encode(encoder{flags: flags, clrs: clrs, indentr: indentr}, b, p)
136 | runtime.KeepAlive(x)
137 | return b, err
138 | }
139 |
140 | // Compact is documented at https://golang.org/pkg/encoding/json/#Compact
141 | func Compact(dst *bytes.Buffer, src []byte) error {
142 | return json.Compact(dst, src)
143 | }
144 |
145 | // HTMLEscape is documented at https://golang.org/pkg/encoding/json/#HTMLEscape
146 | func HTMLEscape(dst *bytes.Buffer, src []byte) {
147 | json.HTMLEscape(dst, src)
148 | }
149 |
150 | // Indent is documented at https://golang.org/pkg/encoding/json/#Indent
151 | func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
152 | return json.Indent(dst, src, prefix, indent)
153 | }
154 |
155 | // Marshal is documented at https://golang.org/pkg/encoding/json/#Marshal
156 | func Marshal(x interface{}) ([]byte, error) {
157 | var err error
158 | buf := encoderBufferPool.Get().(*encoderBuffer) //nolint:errcheck
159 |
160 | if buf.data, err = Append(buf.data[:0], x, EscapeHTML|SortMapKeys, nil, nil); err != nil {
161 | return nil, err
162 | }
163 |
164 | b := make([]byte, len(buf.data))
165 | copy(b, buf.data)
166 | encoderBufferPool.Put(buf)
167 | return b, nil
168 | }
169 |
170 | // MarshalIndent is documented at https://golang.org/pkg/encoding/json/#MarshalIndent
171 | func MarshalIndent(x interface{}, prefix, indent string) ([]byte, error) {
172 | b, err := Marshal(x)
173 |
174 | if err == nil {
175 | tmp := &bytes.Buffer{}
176 | tmp.Grow(2 * len(b))
177 |
178 | if err = Indent(tmp, b, prefix, indent); err != nil {
179 | return b, err
180 | }
181 |
182 | b = tmp.Bytes()
183 | }
184 |
185 | return b, err
186 | }
187 |
188 | // Unmarshal is documented at https://golang.org/pkg/encoding/json/#Unmarshal
189 | func Unmarshal(b []byte, x interface{}) error {
190 | r, err := Parse(b, x, 0)
191 | if len(r) != 0 {
192 | var e *SyntaxError
193 | if !errors.As(err, &e) {
194 | // The encoding/json package prioritizes reporting errors caused by
195 | // unexpected trailing bytes over other issues; here we emulate this
196 | // behavior by overriding the error.
197 | err = syntaxError(r, "invalid character '%c' after top-level value", r[0])
198 | }
199 | }
200 | return err
201 | }
202 |
203 | // Parse behaves like Unmarshal but the caller can pass a set of flags to
204 | // configure the parsing behavior.
205 | func Parse(b []byte, x interface{}, flags ParseFlags) ([]byte, error) {
206 | t := reflect.TypeOf(x)
207 | p := (*iface)(unsafe.Pointer(&x)).ptr
208 |
209 | if t == nil || p == nil || t.Kind() != reflect.Ptr {
210 | _, r, err := parseValue(skipSpaces(b))
211 | r = skipSpaces(r)
212 | if err != nil {
213 | return r, err
214 | }
215 | return r, &InvalidUnmarshalError{Type: t}
216 | }
217 | t = t.Elem()
218 |
219 | cache := cacheLoad()
220 | c, found := cache[typeid(t)]
221 |
222 | if !found {
223 | c = constructCachedCodec(t, cache)
224 | }
225 |
226 | r, err := c.decode(decoder{flags: flags}, skipSpaces(b), p)
227 | return skipSpaces(r), err
228 | }
229 |
230 | // Valid is documented at https://golang.org/pkg/encoding/json/#Valid
231 | func Valid(data []byte) bool {
232 | _, data, err := parseValue(skipSpaces(data))
233 | if err != nil {
234 | return false
235 | }
236 | return len(skipSpaces(data)) == 0
237 | }
238 |
239 | // Decoder is documented at https://golang.org/pkg/encoding/json/#Decoder
240 | type Decoder struct {
241 | reader io.Reader
242 | buffer []byte
243 | remain []byte
244 | inputOffset int64
245 | err error
246 | flags ParseFlags
247 | }
248 |
249 | // NewDecoder is documented at https://golang.org/pkg/encoding/json/#NewDecoder
250 | func NewDecoder(r io.Reader) *Decoder { return &Decoder{reader: r} }
251 |
252 | // Buffered is documented at https://golang.org/pkg/encoding/json/#Decoder.Buffered
253 | func (dec *Decoder) Buffered() io.Reader {
254 | return bytes.NewReader(dec.remain)
255 | }
256 |
257 | // Decode is documented at https://golang.org/pkg/encoding/json/#Decoder.Decode
258 | func (dec *Decoder) Decode(v interface{}) error {
259 | raw, err := dec.readValue()
260 | if err != nil {
261 | return err
262 | }
263 | _, err = Parse(raw, v, dec.flags)
264 | return err
265 | }
266 |
267 | const (
268 | minBufferSize = 32768
269 | minReadSize = 4096
270 | )
271 |
272 | // readValue reads one JSON value from the buffer and returns its raw bytes. It
273 | // is optimized for the "one JSON value per line" case.
274 | func (dec *Decoder) readValue() (v []byte, err error) {
275 | var n int
276 | var r []byte
277 |
278 | for {
279 | if len(dec.remain) != 0 {
280 | v, r, err = parseValue(dec.remain)
281 | if err == nil {
282 | dec.remain, n = skipSpacesN(r)
283 | dec.inputOffset += int64(len(v) + n)
284 | return v, nil
285 | }
286 | if len(r) != 0 {
287 | // Parsing of the next JSON value stopped at a position other
288 | // than the end of the input buffer, which indicaates that a
289 | // syntax error was encountered.
290 | return v, err
291 | }
292 | }
293 |
294 | if err = dec.err; err != nil {
295 | if len(dec.remain) != 0 && errors.Is(err, io.EOF) {
296 | err = io.ErrUnexpectedEOF
297 | }
298 | return v, err
299 | }
300 |
301 | if dec.buffer == nil {
302 | dec.buffer = make([]byte, 0, minBufferSize)
303 | } else {
304 | dec.buffer = dec.buffer[:copy(dec.buffer[:cap(dec.buffer)], dec.remain)]
305 | dec.remain = nil
306 | }
307 |
308 | if (cap(dec.buffer) - len(dec.buffer)) < minReadSize {
309 | buf := make([]byte, len(dec.buffer), 2*cap(dec.buffer))
310 | copy(buf, dec.buffer)
311 | dec.buffer = buf
312 | }
313 |
314 | n, err = io.ReadFull(dec.reader, dec.buffer[len(dec.buffer):cap(dec.buffer)])
315 | if n > 0 {
316 | dec.buffer = dec.buffer[:len(dec.buffer)+n]
317 | if err != nil {
318 | err = nil
319 | }
320 | } else if errors.Is(err, io.ErrUnexpectedEOF) {
321 | err = io.EOF
322 | }
323 | dec.remain, n = skipSpacesN(dec.buffer)
324 | dec.inputOffset += int64(n)
325 | dec.err = err
326 | }
327 | }
328 |
329 | // DisallowUnknownFields is documented at https://golang.org/pkg/encoding/json/#Decoder.DisallowUnknownFields
330 | func (dec *Decoder) DisallowUnknownFields() { dec.flags |= DisallowUnknownFields }
331 |
332 | // UseNumber is documented at https://golang.org/pkg/encoding/json/#Decoder.UseNumber
333 | func (dec *Decoder) UseNumber() { dec.flags |= UseNumber }
334 |
335 | // DontCopyString is an extension to the standard encoding/json package
336 | // which instructs the decoder to not copy strings loaded from the json
337 | // payloads when possible.
338 | func (dec *Decoder) DontCopyString() { dec.flags |= DontCopyString }
339 |
340 | // DontCopyNumber is an extension to the standard encoding/json package
341 | // which instructs the decoder to not copy numbers loaded from the json
342 | // payloads.
343 | func (dec *Decoder) DontCopyNumber() { dec.flags |= DontCopyNumber }
344 |
345 | // DontCopyRawMessage is an extension to the standard encoding/json package
346 | // which instructs the decoder to not allocate RawMessage values in separate
347 | // memory buffers (see the documentation of the DontcopyRawMessage flag for
348 | // more detais).
349 | func (dec *Decoder) DontCopyRawMessage() { dec.flags |= DontCopyRawMessage }
350 |
351 | // DontMatchCaseInsensitiveStructFields is an extension to the standard
352 | // encoding/json package which instructs the decoder to not match object fields
353 | // against struct fields in a case-insensitive way, the field names have to
354 | // match exactly to be decoded into the struct field values.
355 | func (dec *Decoder) DontMatchCaseInsensitiveStructFields() {
356 | dec.flags |= DontMatchCaseInsensitiveStructFields
357 | }
358 |
359 | // ZeroCopy is an extension to the standard encoding/json package which enables
360 | // all the copy optimizations of the decoder.
361 | func (dec *Decoder) ZeroCopy() { dec.flags |= ZeroCopy }
362 |
363 | // InputOffset returns the input stream byte offset of the current decoder position.
364 | // The offset gives the location of the end of the most recently returned token
365 | // and the beginning of the next token.
366 | func (dec *Decoder) InputOffset() int64 {
367 | return dec.inputOffset
368 | }
369 |
370 | // Encoder is documented at https://golang.org/pkg/encoding/json/#Encoder
371 | type Encoder struct {
372 | writer io.Writer
373 | err error
374 | flags AppendFlags
375 | clrs *Colors
376 | indentr *indenter
377 | }
378 |
379 | // NewEncoder is documented at https://golang.org/pkg/encoding/json/#NewEncoder
380 | func NewEncoder(w io.Writer) *Encoder { return &Encoder{writer: w, flags: EscapeHTML | SortMapKeys} }
381 |
382 | // SetColors sets the colors for the encoder to use.
383 | func (enc *Encoder) SetColors(c *Colors) {
384 | enc.clrs = c
385 | }
386 |
387 | // Encode is documented at https://golang.org/pkg/encoding/json/#Encoder.Encode
388 | func (enc *Encoder) Encode(v interface{}) error {
389 | if enc.err != nil {
390 | return enc.err
391 | }
392 |
393 | var err error
394 | buf := encoderBufferPool.Get().(*encoderBuffer) //nolint:errcheck
395 |
396 | // Note: unlike the original segmentio encoder, indentation is
397 | // performed via the Append function.
398 | buf.data, err = Append(buf.data[:0], v, enc.flags, enc.clrs, enc.indentr)
399 | if err != nil {
400 | encoderBufferPool.Put(buf)
401 | return err
402 | }
403 |
404 | buf.data = append(buf.data, '\n')
405 | b := buf.data
406 |
407 | if _, err2 := enc.writer.Write(b); err2 != nil {
408 | enc.err = err2
409 | }
410 |
411 | encoderBufferPool.Put(buf)
412 | return err
413 | }
414 |
415 | // SetEscapeHTML is documented at https://golang.org/pkg/encoding/json/#Encoder.SetEscapeHTML
416 | func (enc *Encoder) SetEscapeHTML(on bool) {
417 | if on {
418 | enc.flags |= EscapeHTML
419 | } else {
420 | enc.flags &= ^EscapeHTML
421 | }
422 | }
423 |
424 | // SetIndent is documented at https://golang.org/pkg/encoding/json/#Encoder.SetIndent
425 | func (enc *Encoder) SetIndent(prefix, indent string) {
426 | enc.indentr = newIndenter(prefix, indent)
427 | }
428 |
429 | // SetSortMapKeys is an extension to the standard encoding/json package which
430 | // allows the program to toggle sorting of map keys on and off.
431 | func (enc *Encoder) SetSortMapKeys(on bool) {
432 | if on {
433 | enc.flags |= SortMapKeys
434 | } else {
435 | enc.flags &= ^SortMapKeys
436 | }
437 | }
438 |
439 | // SetTrustRawMessage skips value checking when encoding a raw json message. It should only
440 | // be used if the values are known to be valid json, e.g. because they were originally created
441 | // by json.Unmarshal.
442 | func (enc *Encoder) SetTrustRawMessage(on bool) {
443 | if on {
444 | enc.flags |= TrustRawMessage
445 | } else {
446 | enc.flags &= ^TrustRawMessage
447 | }
448 | }
449 |
450 | var encoderBufferPool = sync.Pool{
451 | New: func() interface{} { return &encoderBuffer{data: make([]byte, 0, 4096)} },
452 | }
453 |
454 | type encoderBuffer struct{ data []byte }
455 |
--------------------------------------------------------------------------------
/jsoncolor.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import (
4 | "strconv"
5 | )
6 |
7 | // Colors specifies colorization of JSON output. Each field
8 | // is a Color, which is simply the bytes of the terminal color code.
9 | type Colors struct {
10 | // Null is the color for JSON nil.
11 | Null Color
12 |
13 | // Bool is the color for boolean values.
14 | Bool Color
15 |
16 | // Number is the color for number values.
17 | Number Color
18 |
19 | // String is the color for string values.
20 | String Color
21 |
22 | // Key is the color for JSON keys.
23 | Key Color
24 |
25 | // Bytes is the color for byte data.
26 | Bytes Color
27 |
28 | // Time is the color for datetime values.
29 | Time Color
30 |
31 | // Punc is the color for JSON punctuation: []{},: etc.
32 | Punc Color
33 |
34 | // TextMarshaler is the color for values implementing encoding.TextMarshaler.
35 | TextMarshaler Color
36 | }
37 |
38 | // appendNull appends a colorized "null" to b.
39 | func (c *Colors) appendNull(b []byte) []byte {
40 | if c == nil {
41 | return append(b, "null"...)
42 | }
43 |
44 | b = append(b, c.Null...)
45 | b = append(b, "null"...)
46 | return append(b, ansiReset...)
47 | }
48 |
49 | // appendBool appends the colorized bool v to b.
50 | func (c *Colors) appendBool(b []byte, v bool) []byte {
51 | if c == nil {
52 | if v {
53 | return append(b, "true"...)
54 | }
55 |
56 | return append(b, "false"...)
57 | }
58 |
59 | b = append(b, c.Bool...)
60 | if v {
61 | b = append(b, "true"...)
62 | } else {
63 | b = append(b, "false"...)
64 | }
65 |
66 | return append(b, ansiReset...)
67 | }
68 |
69 | // appendInt64 appends the colorized int64 v to b.
70 | func (c *Colors) appendInt64(b []byte, v int64) []byte {
71 | if c == nil {
72 | return strconv.AppendInt(b, v, 10)
73 | }
74 |
75 | b = append(b, c.Number...)
76 | b = strconv.AppendInt(b, v, 10)
77 | return append(b, ansiReset...)
78 | }
79 |
80 | // appendUint64 appends the colorized uint64 v to b.
81 | func (c *Colors) appendUint64(b []byte, v uint64) []byte {
82 | if c == nil {
83 | return strconv.AppendUint(b, v, 10)
84 | }
85 |
86 | b = append(b, c.Number...)
87 | b = strconv.AppendUint(b, v, 10)
88 | return append(b, ansiReset...)
89 | }
90 |
91 | // appendPunc appends the colorized punctuation mark v to b.
92 | func (c *Colors) appendPunc(b []byte, v byte) []byte {
93 | if c == nil {
94 | return append(b, v)
95 | }
96 |
97 | b = append(b, c.Punc...)
98 | b = append(b, v)
99 | return append(b, ansiReset...)
100 | }
101 |
102 | // Color is used to render terminal colors. In effect, Color is
103 | // the bytes of the ANSI prefix code. The zero value is valid (results in
104 | // no colorization). When Color is non-zero, the encoder writes the prefix,
105 | // then the actual value, then the ANSI reset code.
106 | //
107 | // Example value:
108 | //
109 | // number := Color("\x1b[36m")
110 | type Color []byte
111 |
112 | // ansiReset is the ANSI ansiReset escape code.
113 | const ansiReset = "\x1b[0m"
114 |
115 | // DefaultColors returns the default Colors configuration.
116 | // These colors largely follow jq's default colorization,
117 | // with some deviation.
118 | func DefaultColors() *Colors {
119 | return &Colors{
120 | Null: Color("\x1b[2m"),
121 | Bool: Color("\x1b[1m"),
122 | Number: Color("\x1b[36m"),
123 | String: Color("\x1b[32m"),
124 | Key: Color("\x1b[34;1m"),
125 | Bytes: Color("\x1b[2m"),
126 | Time: Color("\x1b[32;2m"),
127 | Punc: Color{}, // No colorization
128 | TextMarshaler: Color("\x1b[32m"), // Same as String
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/jsoncolor_internal_test.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import (
4 | "bytes"
5 | stdjson "encoding/json"
6 | "testing"
7 |
8 | "github.com/segmentio/encoding/json"
9 |
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestEquivalenceStdlibCode(t *testing.T) {
14 | if codeJSON == nil {
15 | codeInit()
16 | }
17 |
18 | bufStdj := &bytes.Buffer{}
19 | err := stdjson.NewEncoder(bufStdj).Encode(codeStruct)
20 | require.NoError(t, err)
21 |
22 | bufSegmentj := &bytes.Buffer{}
23 | err = json.NewEncoder(bufSegmentj).Encode(codeStruct)
24 | require.NoError(t, err)
25 | require.Equal(t, bufStdj.String(), bufSegmentj.String())
26 |
27 | bufJ := &bytes.Buffer{}
28 | err = NewEncoder(bufJ).Encode(codeStruct)
29 | require.Equal(t, bufStdj.String(), bufJ.String())
30 | }
31 |
--------------------------------------------------------------------------------
/jsoncolor_test.go:
--------------------------------------------------------------------------------
1 | package jsoncolor_test
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io/ioutil"
7 | "testing"
8 |
9 | "github.com/segmentio/encoding/json"
10 |
11 | "github.com/neilotoole/jsoncolor"
12 | "github.com/stretchr/testify/require"
13 |
14 | stdjson "encoding/json"
15 | )
16 |
17 | // TestPackageDropIn checks that jsoncolor satisfies basic requirements
18 | // to be a drop-in for encoding/json.
19 | func TestPackageDropIn(t *testing.T) {
20 | // Verify encoding/json types exists
21 | var (
22 | _ = jsoncolor.Decoder{}
23 | _ = jsoncolor.Delim(0)
24 | _ = jsoncolor.Encoder{}
25 | _ = jsoncolor.InvalidUnmarshalError{}
26 | _ = jsoncolor.Marshaler(nil)
27 | _ = jsoncolor.MarshalerError{}
28 | _ = jsoncolor.Number("0")
29 | _ = jsoncolor.RawMessage{}
30 | _ = jsoncolor.SyntaxError{}
31 | _ = jsoncolor.Token(nil)
32 | _ = jsoncolor.UnmarshalTypeError{}
33 | _ = jsoncolor.Unmarshaler(nil)
34 | _ = jsoncolor.UnsupportedTypeError{}
35 | _ = jsoncolor.UnsupportedValueError{}
36 | )
37 |
38 | const prefix, indent = "", " "
39 |
40 | testCases := []string{"testdata/sakila_actor.json", "testdata/sakila_payment.json"}
41 | for _, tc := range testCases {
42 | tc := tc
43 | t.Run(tc, func(t *testing.T) {
44 | b, readErr := ioutil.ReadFile(tc)
45 | require.NoError(t, readErr)
46 |
47 | // Test json.Valid equivalence
48 | fv1, fv2 := json.Valid, jsoncolor.Valid
49 | require.Equal(t, fv1(b), fv2(b))
50 |
51 | // Test json.Unmarshal equivalence
52 | fu1, fu2 := json.Unmarshal, jsoncolor.Unmarshal
53 | var m1, m2 interface{}
54 | err1, err2 := fu1(b, &m1), fu2(b, &m2)
55 | require.NoError(t, err1)
56 | require.NoError(t, err2)
57 | require.EqualValues(t, m1, m2)
58 |
59 | // Test json.Marshal equivalence
60 | fm1, fm2 := json.Marshal, jsoncolor.Marshal
61 | gotMarshalB1, err1 := fm1(m1)
62 | require.NoError(t, err1)
63 | gotMarshalB2, err2 := fm2(m1)
64 | require.NoError(t, err2)
65 | require.Equal(t, gotMarshalB1, gotMarshalB2)
66 |
67 | // Test json.MarshalIndent equivalence
68 | fmi1, fmi2 := json.MarshalIndent, jsoncolor.MarshalIndent
69 | gotMarshallIndentB1, err1 := fmi1(m1, prefix, indent)
70 | require.NoError(t, err1)
71 | gotMarshalIndentB2, err2 := fmi2(m1, prefix, indent)
72 | require.NoError(t, err2)
73 | require.Equal(t, gotMarshallIndentB1, gotMarshalIndentB2)
74 |
75 | // Test json.Compact equivalence
76 | fc1, fc2 := json.Compact, jsoncolor.Compact
77 | buf1, buf2 := &bytes.Buffer{}, &bytes.Buffer{}
78 | err1 = fc1(buf1, gotMarshallIndentB1)
79 | require.NoError(t, err1)
80 | err2 = fc2(buf2, gotMarshalIndentB2)
81 | require.NoError(t, err2)
82 | require.Equal(t, buf1.Bytes(), buf2.Bytes())
83 | // Double-check
84 | require.Equal(t, buf1.Bytes(), gotMarshalB1)
85 | require.Equal(t, buf2.Bytes(), gotMarshalB2)
86 | buf1.Reset()
87 | buf2.Reset()
88 |
89 | // Test json.Indent equivalence
90 | fi1, fi2 := json.Indent, jsoncolor.Indent
91 | err1 = fi1(buf1, gotMarshalB1, prefix, indent)
92 | require.NoError(t, err1)
93 | err2 = fi2(buf2, gotMarshalB2, prefix, indent)
94 | require.NoError(t, err2)
95 | require.Equal(t, buf1.Bytes(), buf2.Bytes())
96 | buf1.Reset()
97 | buf2.Reset()
98 |
99 | // Test json.HTMLEscape equivalence
100 | fh1, fh2 := json.HTMLEscape, jsoncolor.HTMLEscape
101 | fh1(buf1, gotMarshalB1)
102 | fh2(buf2, gotMarshalB2)
103 | require.Equal(t, buf1.Bytes(), buf2.Bytes())
104 | })
105 | }
106 | }
107 |
108 | func TestEncode(t *testing.T) {
109 | testCases := []struct {
110 | name string
111 | pretty bool
112 | color bool
113 | sortMap bool
114 | v interface{}
115 | want string
116 | }{
117 | {name: "nil", pretty: false, v: nil, want: "null\n"},
118 | {name: "slice_empty", pretty: true, v: []int{}, want: "[]\n"},
119 | {name: "slice_1_pretty", pretty: true, v: []interface{}{1}, want: "[\n 1\n]\n"},
120 | {name: "slice_1_no_pretty", v: []interface{}{1}, want: "[1]\n"},
121 | {name: "slice_2_pretty", pretty: true, v: []interface{}{1, true}, want: "[\n 1,\n true\n]\n"},
122 | {name: "slice_2_no_pretty", v: []interface{}{1, true}, want: "[1,true]\n"},
123 | {name: "map_int_empty", pretty: true, v: map[string]int{}, want: "{}\n"},
124 | {name: "map_interface_empty", pretty: true, v: map[string]interface{}{}, want: "{}\n"},
125 | {name: "map_interface_empty_sorted", pretty: true, sortMap: true, v: map[string]interface{}{}, want: "{}\n"},
126 | {name: "map_1_pretty", pretty: true, sortMap: true, v: map[string]interface{}{"one": 1}, want: "{\n \"one\": 1\n}\n"},
127 | {name: "map_1_no_pretty", sortMap: true, v: map[string]interface{}{"one": 1}, want: "{\"one\":1}\n"},
128 | {name: "map_2_pretty", pretty: true, sortMap: true, v: map[string]interface{}{"one": 1, "two": 2}, want: "{\n \"one\": 1,\n \"two\": 2\n}\n"},
129 | {name: "map_2_no_pretty", sortMap: true, v: map[string]interface{}{"one": 1, "two": 2}, want: "{\"one\":1,\"two\":2}\n"},
130 | {name: "tinystruct", pretty: true, v: TinyStruct{FBool: true}, want: "{\n \"f_bool\": true\n}\n"},
131 | }
132 |
133 | for _, tc := range testCases {
134 | tc := tc
135 |
136 | t.Run(tc.name, func(t *testing.T) {
137 | buf := &bytes.Buffer{}
138 | enc := jsoncolor.NewEncoder(buf)
139 | enc.SetEscapeHTML(false)
140 | enc.SetSortMapKeys(tc.sortMap)
141 | if tc.pretty {
142 | enc.SetIndent("", " ")
143 | }
144 | if tc.color {
145 | clrs := jsoncolor.DefaultColors()
146 | enc.SetColors(clrs)
147 | }
148 |
149 | require.NoError(t, enc.Encode(tc.v))
150 | require.True(t, stdjson.Valid(buf.Bytes()))
151 | require.Equal(t, tc.want, buf.String())
152 | })
153 | }
154 | }
155 |
156 | func TestEncode_Slice(t *testing.T) {
157 | testCases := []struct {
158 | name string
159 | pretty bool
160 | color bool
161 | v []interface{}
162 | want string
163 | }{
164 | {name: "nil", pretty: true, v: nil, want: "null\n"},
165 | {name: "empty", pretty: true, v: []interface{}{}, want: "[]\n"},
166 | {name: "one", pretty: true, v: []interface{}{1}, want: "[\n 1\n]\n"},
167 | {name: "two", pretty: true, v: []interface{}{1, true}, want: "[\n 1,\n true\n]\n"},
168 | {name: "three", pretty: true, v: []interface{}{1, true, "hello"}, want: "[\n 1,\n true,\n \"hello\"\n]\n"},
169 | }
170 |
171 | for _, tc := range testCases {
172 | tc := tc
173 |
174 | t.Run(tc.name, func(t *testing.T) {
175 | buf := &bytes.Buffer{}
176 | enc := jsoncolor.NewEncoder(buf)
177 | enc.SetEscapeHTML(false)
178 | if tc.pretty {
179 | enc.SetIndent("", " ")
180 | }
181 | if tc.color {
182 | enc.SetColors(jsoncolor.DefaultColors())
183 | }
184 |
185 | require.NoError(t, enc.Encode(tc.v))
186 | require.True(t, stdjson.Valid(buf.Bytes()))
187 | require.Equal(t, tc.want, buf.String())
188 | })
189 | }
190 | }
191 |
192 | func TestEncode_SmallStruct(t *testing.T) {
193 | v := SmallStruct{
194 | FInt: 7,
195 | FSlice: []interface{}{64, true},
196 | FMap: map[string]interface{}{
197 | "m_float64": 64.64,
198 | "m_string": "hello",
199 | },
200 | FTinyStruct: TinyStruct{FBool: true},
201 | FString: "hello",
202 | }
203 |
204 | testCases := []struct {
205 | pretty bool
206 | color bool
207 | want string
208 | }{
209 | {pretty: false, color: false, want: "{\"f_int\":7,\"f_slice\":[64,true],\"f_map\":{\"m_float64\":64.64,\"m_string\":\"hello\"},\"f_tinystruct\":{\"f_bool\":true},\"f_string\":\"hello\"}\n"},
210 | {pretty: true, color: false, want: "{\n \"f_int\": 7,\n \"f_slice\": [\n 64,\n true\n ],\n \"f_map\": {\n \"m_float64\": 64.64,\n \"m_string\": \"hello\"\n },\n \"f_tinystruct\": {\n \"f_bool\": true\n },\n \"f_string\": \"hello\"\n}\n"},
211 | }
212 |
213 | for _, tc := range testCases {
214 | tc := tc
215 |
216 | t.Run(fmt.Sprintf("pretty_%v__color_%v", tc.pretty, tc.color), func(t *testing.T) {
217 | buf := &bytes.Buffer{}
218 | enc := jsoncolor.NewEncoder(buf)
219 | enc.SetEscapeHTML(false)
220 | enc.SetSortMapKeys(true)
221 |
222 | if tc.pretty {
223 | enc.SetIndent("", " ")
224 | }
225 | if tc.color {
226 | enc.SetColors(jsoncolor.DefaultColors())
227 | }
228 |
229 | require.NoError(t, enc.Encode(v))
230 | require.True(t, stdjson.Valid(buf.Bytes()))
231 | require.Equal(t, tc.want, buf.String())
232 | })
233 | }
234 | }
235 |
236 | func TestEncode_Map_Nested(t *testing.T) {
237 | v := map[string]interface{}{
238 | "m_bool1": true,
239 | "m_nest1": map[string]interface{}{
240 | "m_nest1_bool": true,
241 | "m_nest2": map[string]interface{}{
242 | "m_nest2_bool": true,
243 | "m_nest3": map[string]interface{}{
244 | "m_nest3_bool": true,
245 | },
246 | },
247 | },
248 | "m_string1": "hello",
249 | }
250 |
251 | testCases := []struct {
252 | pretty bool
253 | color bool
254 | want string
255 | }{
256 | {pretty: false, want: "{\"m_bool1\":true,\"m_nest1\":{\"m_nest1_bool\":true,\"m_nest2\":{\"m_nest2_bool\":true,\"m_nest3\":{\"m_nest3_bool\":true}}},\"m_string1\":\"hello\"}\n"},
257 | {pretty: true, want: "{\n \"m_bool1\": true,\n \"m_nest1\": {\n \"m_nest1_bool\": true,\n \"m_nest2\": {\n \"m_nest2_bool\": true,\n \"m_nest3\": {\n \"m_nest3_bool\": true\n }\n }\n },\n \"m_string1\": \"hello\"\n}\n"},
258 | }
259 |
260 | for _, tc := range testCases {
261 | tc := tc
262 |
263 | t.Run(fmt.Sprintf("pretty_%v__color_%v", tc.pretty, tc.color), func(t *testing.T) {
264 | buf := &bytes.Buffer{}
265 | enc := jsoncolor.NewEncoder(buf)
266 | enc.SetEscapeHTML(false)
267 | enc.SetSortMapKeys(true)
268 | if tc.pretty {
269 | enc.SetIndent("", " ")
270 | }
271 | if tc.color {
272 | enc.SetColors(jsoncolor.DefaultColors())
273 | }
274 |
275 | require.NoError(t, enc.Encode(v))
276 | require.True(t, stdjson.Valid(buf.Bytes()))
277 | require.Equal(t, tc.want, buf.String())
278 | })
279 | }
280 | }
281 |
282 | // TestEncode_Map_StringNotInterface tests maps with a string key
283 | // but the value type is not interface{}.
284 | // For example, map[string]bool. This test is necessary because the
285 | // encoder has a fast path for map[string]interface{}
286 | func TestEncode_Map_StringNotInterface(t *testing.T) {
287 | testCases := []struct {
288 | pretty bool
289 | color bool
290 | sortMap bool
291 | v map[string]bool
292 | want string
293 | }{
294 | {pretty: false, sortMap: true, v: map[string]bool{}, want: "{}\n"},
295 | {pretty: false, sortMap: false, v: map[string]bool{}, want: "{}\n"},
296 | {pretty: true, sortMap: true, v: map[string]bool{}, want: "{}\n"},
297 | {pretty: true, sortMap: false, v: map[string]bool{}, want: "{}\n"},
298 | {pretty: false, sortMap: true, v: map[string]bool{"one": true}, want: "{\"one\":true}\n"},
299 | {pretty: false, sortMap: false, v: map[string]bool{"one": true}, want: "{\"one\":true}\n"},
300 | {pretty: false, sortMap: true, v: map[string]bool{"one": true, "two": false}, want: "{\"one\":true,\"two\":false}\n"},
301 | {pretty: true, sortMap: true, v: map[string]bool{"one": true, "two": false}, want: "{\n \"one\": true,\n \"two\": false\n}\n"},
302 | }
303 |
304 | for _, tc := range testCases {
305 | tc := tc
306 |
307 | t.Run(fmt.Sprintf("size_%d__pretty_%v__color_%v", len(tc.v), tc.pretty, tc.color), func(t *testing.T) {
308 | buf := &bytes.Buffer{}
309 | enc := jsoncolor.NewEncoder(buf)
310 | enc.SetEscapeHTML(false)
311 | enc.SetSortMapKeys(tc.sortMap)
312 | if tc.pretty {
313 | enc.SetIndent("", " ")
314 | }
315 | if tc.color {
316 | enc.SetColors(jsoncolor.DefaultColors())
317 | }
318 |
319 | require.NoError(t, enc.Encode(tc.v))
320 | require.True(t, stdjson.Valid(buf.Bytes()))
321 | require.Equal(t, tc.want, buf.String())
322 | })
323 | }
324 | }
325 |
326 | func TestEncode_RawMessage(t *testing.T) {
327 | type RawStruct struct {
328 | FString string `json:"f_string"`
329 | FRaw jsoncolor.RawMessage `json:"f_raw"`
330 | }
331 |
332 | raw := jsoncolor.RawMessage(`{"one":1,"two":2}`)
333 |
334 | testCases := []struct {
335 | name string
336 | pretty bool
337 | color bool
338 | v interface{}
339 | want string
340 | }{
341 | {name: "empty", pretty: false, v: jsoncolor.RawMessage(`{}`), want: "{}\n"},
342 | {name: "no_pretty", pretty: false, v: raw, want: "{\"one\":1,\"two\":2}\n"},
343 | {name: "pretty", pretty: true, v: raw, want: "{\n \"one\": 1,\n \"two\": 2\n}\n"},
344 | {name: "pretty_struct", pretty: true, v: RawStruct{FString: "hello", FRaw: raw}, want: "{\n \"f_string\": \"hello\",\n \"f_raw\": {\n \"one\": 1,\n \"two\": 2\n }\n}\n"},
345 | }
346 |
347 | for _, tc := range testCases {
348 | tc := tc
349 |
350 | t.Run(tc.name, func(t *testing.T) {
351 | buf := &bytes.Buffer{}
352 | enc := jsoncolor.NewEncoder(buf)
353 | enc.SetEscapeHTML(false)
354 | enc.SetSortMapKeys(true)
355 | if tc.pretty {
356 | enc.SetIndent("", " ")
357 | }
358 | if tc.color {
359 | enc.SetColors(jsoncolor.DefaultColors())
360 | }
361 |
362 | err := enc.Encode(tc.v)
363 | require.NoError(t, err)
364 | require.True(t, stdjson.Valid(buf.Bytes()))
365 | require.Equal(t, tc.want, buf.String())
366 | })
367 | }
368 | }
369 |
370 | // TestEncode_Map_StringNotInterface tests map[string]json.RawMessage.
371 | // This test is necessary because the encoder has a fast path
372 | // for map[string]interface{}.
373 | func TestEncode_Map_StringRawMessage(t *testing.T) {
374 | t.Skipf(`Skipping due to intermittent behavior.
375 | See: https://github.com/neilotoole/jsoncolor/issues/19`)
376 | raw := jsoncolor.RawMessage(`{"one":1,"two":2}`)
377 |
378 | testCases := []struct {
379 | pretty bool
380 | color bool
381 | sortMap bool
382 | v map[string]jsoncolor.RawMessage
383 | want string
384 | }{
385 | {pretty: false, sortMap: true, v: map[string]jsoncolor.RawMessage{}, want: "{}\n"},
386 | {pretty: false, sortMap: false, v: map[string]jsoncolor.RawMessage{}, want: "{}\n"},
387 | {pretty: true, sortMap: true, v: map[string]jsoncolor.RawMessage{}, want: "{}\n"},
388 | {pretty: true, sortMap: false, v: map[string]jsoncolor.RawMessage{}, want: "{}\n"},
389 | {pretty: false, sortMap: true, v: map[string]jsoncolor.RawMessage{"msg1": raw, "msg2": raw}, want: "{\"msg1\":{\"one\":1,\"two\":2},\"msg2\":{\"one\":1,\"two\":2}}\n"},
390 | {pretty: true, sortMap: true, v: map[string]jsoncolor.RawMessage{"msg1": raw, "msg2": raw}, want: "{\n \"msg1\": {\n \"one\": 1,\n \"two\": 2\n },\n \"msg2\": {\n \"one\": 1,\n \"two\": 2\n }\n}\n"},
391 | {pretty: true, sortMap: false, v: map[string]jsoncolor.RawMessage{"msg1": raw}, want: "{\n \"msg1\": {\n \"one\": 1,\n \"two\": 2\n }\n}\n"},
392 | }
393 |
394 | for _, tc := range testCases {
395 | tc := tc
396 |
397 | name := fmt.Sprintf("size_%d__pretty_%v__color_%v__sort_%v", len(tc.v), tc.pretty, tc.color, tc.sortMap)
398 | t.Run(name, func(t *testing.T) {
399 | buf := &bytes.Buffer{}
400 | enc := jsoncolor.NewEncoder(buf)
401 | enc.SetEscapeHTML(false)
402 | enc.SetSortMapKeys(tc.sortMap)
403 | if tc.pretty {
404 | enc.SetIndent("", " ")
405 | }
406 | if tc.color {
407 | enc.SetColors(jsoncolor.DefaultColors())
408 | }
409 |
410 | require.NoError(t, enc.Encode(tc.v))
411 | require.True(t, stdjson.Valid(buf.Bytes()))
412 | require.Equal(t, tc.want, buf.String())
413 | })
414 | }
415 | }
416 |
417 | func TestEncode_BigStruct(t *testing.T) {
418 | v := newBigStruct()
419 |
420 | testCases := []struct {
421 | pretty bool
422 | color bool
423 | want string
424 | }{
425 | {pretty: false, want: "{\"f_int\":-7,\"f_int8\":-8,\"f_int16\":-16,\"f_int32\":-32,\"f_int64\":-64,\"f_uint\":7,\"f_uint8\":8,\"f_uint16\":16,\"f_uint32\":32,\"f_uint64\":64,\"f_float32\":32.32,\"f_float64\":64.64,\"f_bool\":true,\"f_bytes\":\"aGVsbG8=\",\"f_nil\":null,\"f_string\":\"hello\",\"f_map\":{\"m_bool\":true,\"m_int64\":64,\"m_nil\":null,\"m_smallstruct\":{\"f_int\":7,\"f_slice\":[64,true],\"f_map\":{\"m_float64\":64.64,\"m_string\":\"hello\"},\"f_tinystruct\":{\"f_bool\":true},\"f_string\":\"hello\"},\"m_string\":\"hello\"},\"f_smallstruct\":{\"f_int\":7,\"f_slice\":[64,true],\"f_map\":{\"m_float64\":64.64,\"m_string\":\"hello\"},\"f_tinystruct\":{\"f_bool\":true},\"f_string\":\"hello\"},\"f_interface\":\"hello\",\"f_interfaces\":[64,\"hello\",true]}\n"},
426 | {pretty: true, want: "{\n \"f_int\": -7,\n \"f_int8\": -8,\n \"f_int16\": -16,\n \"f_int32\": -32,\n \"f_int64\": -64,\n \"f_uint\": 7,\n \"f_uint8\": 8,\n \"f_uint16\": 16,\n \"f_uint32\": 32,\n \"f_uint64\": 64,\n \"f_float32\": 32.32,\n \"f_float64\": 64.64,\n \"f_bool\": true,\n \"f_bytes\": \"aGVsbG8=\",\n \"f_nil\": null,\n \"f_string\": \"hello\",\n \"f_map\": {\n \"m_bool\": true,\n \"m_int64\": 64,\n \"m_nil\": null,\n \"m_smallstruct\": {\n \"f_int\": 7,\n \"f_slice\": [\n 64,\n true\n ],\n \"f_map\": {\n \"m_float64\": 64.64,\n \"m_string\": \"hello\"\n },\n \"f_tinystruct\": {\n \"f_bool\": true\n },\n \"f_string\": \"hello\"\n },\n \"m_string\": \"hello\"\n },\n \"f_smallstruct\": {\n \"f_int\": 7,\n \"f_slice\": [\n 64,\n true\n ],\n \"f_map\": {\n \"m_float64\": 64.64,\n \"m_string\": \"hello\"\n },\n \"f_tinystruct\": {\n \"f_bool\": true\n },\n \"f_string\": \"hello\"\n },\n \"f_interface\": \"hello\",\n \"f_interfaces\": [\n 64,\n \"hello\",\n true\n ]\n}\n"},
427 | }
428 |
429 | for _, tc := range testCases {
430 | tc := tc
431 |
432 | t.Run(fmt.Sprintf("pretty_%v__color_%v", tc.pretty, tc.color), func(t *testing.T) {
433 | buf := &bytes.Buffer{}
434 | enc := jsoncolor.NewEncoder(buf)
435 | enc.SetEscapeHTML(false)
436 | enc.SetSortMapKeys(true)
437 | if tc.pretty {
438 | enc.SetIndent("", " ")
439 | }
440 | if tc.color {
441 | enc.SetColors(jsoncolor.DefaultColors())
442 | }
443 |
444 | require.NoError(t, enc.Encode(v))
445 | require.True(t, stdjson.Valid(buf.Bytes()))
446 | require.Equal(t, tc.want, buf.String())
447 | })
448 | }
449 | }
450 |
451 | // TestEncode_Map_Not_StringInterface tests map encoding where
452 | // the map is not map[string]interface{} (for which the encoder
453 | // has a fast path).
454 | //
455 | // NOTE: Currently the encoder is broken wrt colors enabled
456 | // for non-string map keys, though that is kinda JSON-illegal anyway.
457 | func TestEncode_Map_Not_StringInterface(t *testing.T) {
458 | buf := &bytes.Buffer{}
459 | enc := jsoncolor.NewEncoder(buf)
460 | enc.SetEscapeHTML(false)
461 | enc.SetSortMapKeys(true)
462 | enc.SetColors(jsoncolor.DefaultColors())
463 | enc.SetIndent("", " ")
464 |
465 | v := map[int32]string{
466 | 0: "zero",
467 | 1: "one",
468 | 2: "two",
469 | }
470 |
471 | require.NoError(t, enc.Encode(v))
472 | require.False(t, stdjson.Valid(buf.Bytes()),
473 | "expected to be invalid JSON because the encoder currently doesn't handle maps with non-string keys")
474 | }
475 |
476 | // BigStruct is a big test struct.
477 | type BigStruct struct {
478 | FInt int `json:"f_int"`
479 | FInt8 int8 `json:"f_int8"`
480 | FInt16 int16 `json:"f_int16"`
481 | FInt32 int32 `json:"f_int32"`
482 | FInt64 int64 `json:"f_int64"`
483 | FUint uint `json:"f_uint"`
484 | FUint8 uint8 `json:"f_uint8"`
485 | FUint16 uint16 `json:"f_uint16"`
486 | FUint32 uint32 `json:"f_uint32"`
487 | FUint64 uint64 `json:"f_uint64"`
488 | FFloat32 float32 `json:"f_float32"`
489 | FFloat64 float64 `json:"f_float64"`
490 | FBool bool `json:"f_bool"`
491 | FBytes []byte `json:"f_bytes"`
492 | FNil interface{} `json:"f_nil"`
493 | FString string `json:"f_string"`
494 | FMap map[string]interface{} `json:"f_map"`
495 | FSmallStruct SmallStruct `json:"f_smallstruct"`
496 | FInterface interface{} `json:"f_interface"`
497 | FInterfaces []interface{} `json:"f_interfaces"`
498 | }
499 |
500 | // SmallStruct is a small test struct.
501 | type SmallStruct struct {
502 | FInt int `json:"f_int"`
503 | FSlice []interface{} `json:"f_slice"`
504 | FMap map[string]interface{} `json:"f_map"`
505 | FTinyStruct TinyStruct `json:"f_tinystruct"`
506 | FString string `json:"f_string"`
507 | }
508 |
509 | // Tiny Struct is a tiny test struct.
510 | type TinyStruct struct {
511 | FBool bool `json:"f_bool"`
512 | }
513 |
514 | func newBigStruct() BigStruct {
515 | return BigStruct{
516 | FInt: -7,
517 | FInt8: -8,
518 | FInt16: -16,
519 | FInt32: -32,
520 | FInt64: -64,
521 | FUint: 7,
522 | FUint8: 8,
523 | FUint16: 16,
524 | FUint32: 32,
525 | FUint64: 64,
526 | FFloat32: 32.32,
527 | FFloat64: 64.64,
528 | FBool: true,
529 | FBytes: []byte("hello"),
530 | FNil: nil,
531 | FString: "hello",
532 | FMap: map[string]interface{}{
533 | "m_int64": int64(64),
534 | "m_string": "hello",
535 | "m_bool": true,
536 | "m_nil": nil,
537 | "m_smallstruct": newSmallStruct(),
538 | },
539 | FSmallStruct: newSmallStruct(),
540 | FInterface: interface{}("hello"),
541 | FInterfaces: []interface{}{int64(64), "hello", true},
542 | }
543 | }
544 |
545 | func newSmallStruct() SmallStruct {
546 | return SmallStruct{
547 | FInt: 7,
548 | FSlice: []interface{}{64, true},
549 | FMap: map[string]interface{}{
550 | "m_float64": 64.64,
551 | "m_string": "hello",
552 | },
553 | FTinyStruct: TinyStruct{FBool: true},
554 | FString: "hello",
555 | }
556 | }
557 |
558 | func TestEquivalenceRecords(t *testing.T) {
559 | rec := makeRecords(t, 10000)[0]
560 |
561 | bufStdj := &bytes.Buffer{}
562 | err := stdjson.NewEncoder(bufStdj).Encode(rec)
563 | require.NoError(t, err)
564 |
565 | bufSegmentj := &bytes.Buffer{}
566 | err = json.NewEncoder(bufSegmentj).Encode(rec)
567 | require.NoError(t, err)
568 | require.NotEqual(t, bufStdj.String(), bufSegmentj.String(), "segmentj encodes time.Duration to string; stdlib does not")
569 |
570 | bufJ := &bytes.Buffer{}
571 | err = jsoncolor.NewEncoder(bufJ).Encode(rec)
572 | require.Equal(t, bufStdj.String(), bufJ.String())
573 | }
574 |
575 | // TextMarshaler implements encoding.TextMarshaler
576 | type TextMarshaler struct {
577 | Text string
578 | }
579 |
580 | func (t TextMarshaler) MarshalText() ([]byte, error) {
581 | return []byte(t.Text), nil
582 | }
583 |
584 | func TestEncode_TextMarshaler(t *testing.T) {
585 | buf := &bytes.Buffer{}
586 | enc := jsoncolor.NewEncoder(buf)
587 | enc.SetColors(&jsoncolor.Colors{
588 | TextMarshaler: jsoncolor.Color("\x1b[36m"),
589 | })
590 |
591 | text := TextMarshaler{Text: "example text"}
592 |
593 | require.NoError(t, enc.Encode(text))
594 | require.Equal(t, "\x1b[36m\"example text\"\x1b[0m\n", buf.String(),
595 | "expected TextMarshaler encoding to use Colors.TextMarshaler")
596 | }
597 |
--------------------------------------------------------------------------------
/parse.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import (
4 | "bytes"
5 | "math"
6 | "reflect"
7 | "unicode"
8 | "unicode/utf16"
9 | "unicode/utf8"
10 | )
11 |
12 | // All spaces characters defined in the json specification.
13 | const (
14 | sp = ' '
15 | ht = '\t'
16 | nl = '\n'
17 | cr = '\r'
18 | )
19 |
20 | func skipSpaces(b []byte) []byte {
21 | b, _ = skipSpacesN(b)
22 | return b
23 | }
24 |
25 | func skipSpacesN(b []byte) ([]byte, int) {
26 | for i := range b {
27 | switch b[i] {
28 | case sp, ht, nl, cr:
29 | default:
30 | return b[i:], i
31 | }
32 | }
33 | return nil, 0
34 | }
35 |
36 | // parseInt parses a decimanl representation of an int64 from b.
37 | //
38 | // The function is equivalent to calling strconv.ParseInt(string(b), 10, 64) but
39 | // it prevents Go from making a memory allocation for converting a byte slice to
40 | // a string (escape analysis fails due to the error returned by strconv.ParseInt).
41 | //
42 | // Because it only works with base 10 the function is also significantly faster
43 | // than strconv.ParseInt.
44 | func parseInt(b []byte, t reflect.Type) (int64, []byte, error) {
45 | var value int64
46 | var count int
47 |
48 | if len(b) == 0 {
49 | return 0, b, syntaxError(b, "cannot decode integer from an empty input")
50 | }
51 |
52 | if b[0] == '-' {
53 | const max = math.MinInt64
54 | const lim = max / 10
55 |
56 | if len(b) == 1 {
57 | return 0, b, syntaxError(b, "cannot decode integer from '-'")
58 | }
59 |
60 | if len(b) > 2 && b[1] == '0' && '0' <= b[2] && b[2] <= '9' {
61 | return 0, b, syntaxError(b, "invalid leading character '0' in integer")
62 | }
63 |
64 | for _, d := range b[1:] {
65 | if !(d >= '0' && d <= '9') {
66 | if count == 0 {
67 | bs, err := inputError(b, t)
68 | return 0, bs, err
69 | }
70 | break
71 | }
72 |
73 | if value < lim {
74 | return 0, b, unmarshalOverflow(b, t)
75 | }
76 |
77 | value *= 10
78 | x := int64(d - '0')
79 |
80 | if value < (max + x) {
81 | return 0, b, unmarshalOverflow(b, t)
82 | }
83 |
84 | value -= x
85 | count++
86 | }
87 |
88 | count++
89 | } else {
90 | const max = math.MaxInt64
91 | const lim = max / 10
92 |
93 | if len(b) > 1 && b[0] == '0' && '0' <= b[1] && b[1] <= '9' {
94 | return 0, b, syntaxError(b, "invalid leading character '0' in integer")
95 | }
96 |
97 | for _, d := range b {
98 | if !(d >= '0' && d <= '9') {
99 | if count == 0 {
100 | bs, err := inputError(b, t)
101 | return 0, bs, err
102 | }
103 | break
104 | }
105 | x := int64(d - '0')
106 |
107 | if value > lim {
108 | return 0, b, unmarshalOverflow(b, t)
109 | }
110 |
111 | if value *= 10; value > (max - x) {
112 | return 0, b, unmarshalOverflow(b, t)
113 | }
114 |
115 | value += x
116 | count++
117 | }
118 | }
119 |
120 | if count < len(b) {
121 | switch b[count] {
122 | case '.', 'e', 'E': // was this actually a float?
123 | v, r, err := parseNumber(b)
124 | if err != nil {
125 | v, r = b[:count+1], b[count+1:]
126 | }
127 | return 0, r, unmarshalTypeError(v, t)
128 | }
129 | }
130 |
131 | return value, b[count:], nil
132 | }
133 |
134 | // parseUint is like parseInt but for unsigned integers.
135 | func parseUint(b []byte, t reflect.Type) (uint64, []byte, error) {
136 | const max = math.MaxUint64
137 | const lim = max / 10
138 |
139 | var value uint64
140 | var count int
141 |
142 | if len(b) == 0 {
143 | return 0, b, syntaxError(b, "cannot decode integer value from an empty input")
144 | }
145 |
146 | if len(b) > 1 && b[0] == '0' && '0' <= b[1] && b[1] <= '9' {
147 | return 0, b, syntaxError(b, "invalid leading character '0' in integer")
148 | }
149 |
150 | for _, d := range b {
151 | if !(d >= '0' && d <= '9') {
152 | if count == 0 {
153 | bs, err := inputError(b, t)
154 | return 0, bs, err
155 | }
156 | break
157 | }
158 | x := uint64(d - '0')
159 |
160 | if value > lim {
161 | return 0, b, unmarshalOverflow(b, t)
162 | }
163 |
164 | if value *= 10; value > (max - x) {
165 | return 0, b, unmarshalOverflow(b, t)
166 | }
167 |
168 | value += x
169 | count++
170 | }
171 |
172 | if count < len(b) {
173 | switch b[count] {
174 | case '.', 'e', 'E': // was this actually a float?
175 | v, r, err := parseNumber(b)
176 | if err != nil {
177 | v, r = b[:count+1], b[count+1:]
178 | }
179 | return 0, r, unmarshalTypeError(v, t)
180 | }
181 | }
182 |
183 | return value, b[count:], nil
184 | }
185 |
186 | // parseUintHex parses a hexadecimanl representation of a uint64 from b.
187 | //
188 | // The function is equivalent to calling strconv.ParseUint(string(b), 16, 64) but
189 | // it prevents Go from making a memory allocation for converting a byte slice to
190 | // a string (escape analysis fails due to the error returned by strconv.ParseUint).
191 | //
192 | // Because it only works with base 16 the function is also significantly faster
193 | // than strconv.ParseUint.
194 | func parseUintHex(b []byte) (uint64, []byte, error) {
195 | const max = math.MaxUint64
196 | const lim = max / 0x10
197 |
198 | var value uint64
199 | var count int
200 |
201 | if len(b) == 0 {
202 | return 0, b, syntaxError(b, "cannot decode hexadecimal value from an empty input")
203 | }
204 |
205 | parseLoop:
206 | for i, d := range b {
207 | var x uint64
208 |
209 | switch {
210 | case d >= '0' && d <= '9':
211 | x = uint64(d - '0')
212 |
213 | case d >= 'A' && d <= 'F':
214 | x = uint64(d-'A') + 0xA
215 |
216 | case d >= 'a' && d <= 'f':
217 | x = uint64(d-'a') + 0xA
218 |
219 | default:
220 | if i == 0 {
221 | return 0, b, syntaxError(b, "expected hexadecimal digit but found '%c'", d)
222 | }
223 | break parseLoop
224 | }
225 |
226 | if value > lim {
227 | return 0, b, syntaxError(b, "hexadecimal value out of range")
228 | }
229 |
230 | if value *= 0x10; value > (max - x) {
231 | return 0, b, syntaxError(b, "hexadecimal value out of range")
232 | }
233 |
234 | value += x
235 | count++
236 | }
237 |
238 | return value, b[count:], nil
239 | }
240 |
241 | func parseNull(b []byte) ([]byte, []byte, error) {
242 | if hasNullPrefix(b) {
243 | return b[:4], b[4:], nil
244 | }
245 | if len(b) < 4 {
246 | return nil, b[len(b):], unexpectedEOF(b)
247 | }
248 | return nil, b, syntaxError(b, "expected 'null' but found invalid token")
249 | }
250 |
251 | func parseTrue(b []byte) ([]byte, []byte, error) {
252 | if hasTruePrefix(b) {
253 | return b[:4], b[4:], nil
254 | }
255 | if len(b) < 4 {
256 | return nil, b[len(b):], unexpectedEOF(b)
257 | }
258 | return nil, b, syntaxError(b, "expected 'true' but found invalid token")
259 | }
260 |
261 | func parseFalse(b []byte) ([]byte, []byte, error) {
262 | if hasFalsePrefix(b) {
263 | return b[:5], b[5:], nil
264 | }
265 | if len(b) < 5 {
266 | return nil, b[len(b):], unexpectedEOF(b)
267 | }
268 | return nil, b, syntaxError(b, "expected 'false' but found invalid token")
269 | }
270 |
271 | func parseNumber(b []byte) (v, r []byte, err error) {
272 | if len(b) == 0 {
273 | r, err = b, unexpectedEOF(b)
274 | return v, r, err
275 | }
276 |
277 | i := 0
278 | // sign
279 | if b[i] == '-' {
280 | i++
281 | }
282 |
283 | if i == len(b) {
284 | r, err = b[i:], syntaxError(b, "missing number value after sign")
285 | return v, r, err
286 | }
287 |
288 | if b[i] < '0' || b[i] > '9' {
289 | r, err = b[i:], syntaxError(b, "expected digit but got '%c'", b[i])
290 | return v, r, err
291 | }
292 |
293 | // integer part
294 | if b[i] == '0' {
295 | i++
296 | if i == len(b) || (b[i] != '.' && b[i] != 'e' && b[i] != 'E') {
297 | v, r = b[:i], b[i:]
298 | return v, r, err
299 | }
300 | if '0' <= b[i] && b[i] <= '9' {
301 | r, err = b[i:], syntaxError(b, "cannot decode number with leading '0' character")
302 | return v, r, err
303 | }
304 | }
305 |
306 | for i < len(b) && '0' <= b[i] && b[i] <= '9' {
307 | i++
308 | }
309 |
310 | // decimal part
311 | if i < len(b) && b[i] == '.' {
312 | i++
313 | decimalStart := i
314 |
315 | for i < len(b) {
316 | if c := b[i]; !('0' <= c && c <= '9') {
317 | if i == decimalStart {
318 | r, err = b[i:], syntaxError(b, "expected digit but found '%c'", c)
319 | return v, r, err
320 | }
321 | break
322 | }
323 | i++
324 | }
325 |
326 | if i == decimalStart {
327 | r, err = b[i:], syntaxError(b, "expected decimal part after '.'")
328 | return v, r, err
329 | }
330 | }
331 |
332 | // exponent part
333 | if i < len(b) && (b[i] == 'e' || b[i] == 'E') {
334 | i++
335 |
336 | if i < len(b) {
337 | if c := b[i]; c == '+' || c == '-' {
338 | i++
339 | }
340 | }
341 |
342 | if i == len(b) {
343 | r, err = b[i:], syntaxError(b, "missing exponent in number")
344 | return v, r, err
345 | }
346 |
347 | exponentStart := i
348 |
349 | for i < len(b) {
350 | if c := b[i]; !('0' <= c && c <= '9') {
351 | if i == exponentStart {
352 | err = syntaxError(b, "expected digit but found '%c'", c)
353 | return v, r, err
354 | }
355 | break
356 | }
357 | i++
358 | }
359 | }
360 |
361 | v, r = b[:i], b[i:]
362 | return v, r, err
363 | }
364 |
365 | func parseUnicode(b []byte) (rune, int, error) {
366 | if len(b) < 4 {
367 | return 0, 0, syntaxError(b, "unicode code point must have at least 4 characters")
368 | }
369 |
370 | u, r, err := parseUintHex(b[:4])
371 | if err != nil {
372 | return 0, 0, syntaxError(b, "parsing unicode code point: %s", err)
373 | }
374 |
375 | if len(r) != 0 {
376 | return 0, 0, syntaxError(b, "invalid unicode code point")
377 | }
378 |
379 | return rune(u), 4, nil
380 | }
381 |
382 | func parseStringFast(b []byte) ([]byte, []byte, bool, error) {
383 | if len(b) < 2 {
384 | return nil, b[len(b):], false, unexpectedEOF(b)
385 | }
386 | if b[0] != '"' {
387 | return nil, b, false, syntaxError(b, "expected '\"' at the beginning of a string value")
388 | }
389 |
390 | n := bytes.IndexByte(b[1:], '"') + 2
391 | if n <= 1 {
392 | return nil, b[len(b):], false, syntaxError(b, "missing '\"' at the end of a string value")
393 | }
394 | if bytes.IndexByte(b[1:n], '\\') < 0 && asciiValidPrint(b[1:n]) {
395 | return b[:n], b[n:], false, nil
396 | }
397 |
398 | for i := 1; i < len(b); i++ {
399 | switch b[i] {
400 | case '\\':
401 | if i++; i < len(b) {
402 | switch b[i] {
403 | case '"', '\\', '/', 'n', 'r', 't', 'f', 'b':
404 | case 'u':
405 | _, n, err := parseUnicode(b[i+1:])
406 | if err != nil {
407 | return nil, b, false, err
408 | }
409 | i += n
410 | default:
411 | return nil, b, false, syntaxError(b, "invalid character '%c' in string escape code", b[i])
412 | }
413 | }
414 |
415 | case '"':
416 | return b[:i+1], b[i+1:], true, nil
417 |
418 | default:
419 | if b[i] < 0x20 {
420 | return nil, b, false, syntaxError(b, "invalid character '%c' in string escape code", b[i])
421 | }
422 | }
423 | }
424 |
425 | return nil, b[len(b):], false, syntaxError(b, "missing '\"' at the end of a string value")
426 | }
427 |
428 | func parseString(b []byte) ([]byte, []byte, error) {
429 | s, b, _, err := parseStringFast(b)
430 | return s, b, err
431 | }
432 |
433 | func parseStringUnquote(b, r []byte) ([]byte, []byte, bool, error) {
434 | s, b, escaped, err := parseStringFast(b)
435 | if err != nil {
436 | return s, b, false, err
437 | }
438 |
439 | s = s[1 : len(s)-1] // trim the quotes
440 |
441 | if !escaped {
442 | return s, b, false, nil
443 | }
444 |
445 | if r == nil {
446 | r = make([]byte, 0, len(s))
447 | }
448 |
449 | for len(s) != 0 {
450 | i := bytes.IndexByte(s, '\\')
451 |
452 | if i < 0 {
453 | r = appendCoerceInvalidUTF8(r, s)
454 | break
455 | }
456 |
457 | r = appendCoerceInvalidUTF8(r, s[:i])
458 | s = s[i+1:]
459 |
460 | c := s[0]
461 | switch c {
462 | case '"', '\\', '/':
463 | // simple escaped character
464 | case 'n':
465 | c = '\n'
466 |
467 | case 'r':
468 | c = '\r'
469 |
470 | case 't':
471 | c = '\t'
472 |
473 | case 'b':
474 | c = '\b'
475 |
476 | case 'f':
477 | c = '\f'
478 |
479 | case 'u':
480 | s = s[1:]
481 |
482 | r1, n1, err := parseUnicode(s)
483 | if err != nil {
484 | return r, b, true, err
485 | }
486 | s = s[n1:]
487 |
488 | if utf16.IsSurrogate(r1) {
489 | if !hasPrefix(s, `\u`) {
490 | r1 = unicode.ReplacementChar
491 | } else {
492 | r2, n2, err := parseUnicode(s[2:])
493 | if err != nil {
494 | return r, b, true, err
495 | }
496 | if r1 = utf16.DecodeRune(r1, r2); r1 != unicode.ReplacementChar {
497 | s = s[2+n2:]
498 | }
499 | }
500 | }
501 |
502 | r = appendRune(r, r1)
503 | continue
504 |
505 | default: // not sure what this escape sequence is
506 | return r, b, false, syntaxError(s, "invalid character '%c' in string escape code", c)
507 | }
508 |
509 | r = append(r, c)
510 | s = s[1:]
511 | }
512 |
513 | return r, b, true, nil
514 | }
515 |
516 | func appendRune(b []byte, r rune) []byte {
517 | n := len(b)
518 | b = append(b, 0, 0, 0, 0)
519 | return b[:n+utf8.EncodeRune(b[n:], r)]
520 | }
521 |
522 | func appendCoerceInvalidUTF8(b, s []byte) []byte {
523 | c := [4]byte{}
524 |
525 | for _, r := range string(s) {
526 | b = append(b, c[:utf8.EncodeRune(c[:], r)]...)
527 | }
528 |
529 | return b
530 | }
531 |
532 | func parseObject(b []byte) ([]byte, []byte, error) {
533 | if len(b) < 2 {
534 | return nil, b[len(b):], unexpectedEOF(b)
535 | }
536 |
537 | if b[0] != '{' {
538 | return nil, b, syntaxError(b, "expected '{' at the beginning of an object value")
539 | }
540 |
541 | var err error
542 | a := b
543 | n := len(b)
544 | i := 0
545 |
546 | b = b[1:]
547 | for {
548 | b = skipSpaces(b)
549 |
550 | if len(b) == 0 {
551 | return nil, b, syntaxError(b, "cannot decode object from empty input")
552 | }
553 |
554 | if b[0] == '}' {
555 | j := (n - len(b)) + 1
556 | return a[:j], a[j:], nil
557 | }
558 |
559 | if i != 0 {
560 | if len(b) == 0 {
561 | return nil, b, syntaxError(b, "unexpected EOF after object field value")
562 | }
563 | if b[0] != ',' {
564 | return nil, b, syntaxError(b, "expected ',' after object field value but found '%c'", b[0])
565 | }
566 | b = skipSpaces(b[1:])
567 | if len(b) == 0 {
568 | return nil, b, unexpectedEOF(b)
569 | }
570 | if b[0] == '}' {
571 | return nil, b, syntaxError(b, "unexpected trailing comma after object field")
572 | }
573 | }
574 |
575 | _, b, err = parseString(b)
576 | if err != nil {
577 | return nil, b, err
578 | }
579 | b = skipSpaces(b)
580 |
581 | if len(b) == 0 {
582 | return nil, b, syntaxError(b, "unexpected EOF after object field key")
583 | }
584 | if b[0] != ':' {
585 | return nil, b, syntaxError(b, "expected ':' after object field key but found '%c'", b[0])
586 | }
587 | b = skipSpaces(b[1:])
588 |
589 | _, b, err = parseValue(b)
590 | if err != nil {
591 | return nil, b, err
592 | }
593 |
594 | i++
595 | }
596 | }
597 |
598 | func parseArray(b []byte) ([]byte, []byte, error) {
599 | if len(b) < 2 {
600 | return nil, b[len(b):], unexpectedEOF(b)
601 | }
602 |
603 | if b[0] != '[' {
604 | return nil, b, syntaxError(b, "expected '[' at the beginning of array value")
605 | }
606 |
607 | var err error
608 | a := b
609 | n := len(b)
610 | i := 0
611 |
612 | b = b[1:]
613 | for {
614 | b = skipSpaces(b)
615 |
616 | if len(b) == 0 {
617 | return nil, b, syntaxError(b, "missing closing ']' after array value")
618 | }
619 |
620 | if b[0] == ']' {
621 | j := (n - len(b)) + 1
622 | return a[:j], a[j:], nil
623 | }
624 |
625 | if i != 0 {
626 | if len(b) == 0 {
627 | return nil, b, syntaxError(b, "unexpected EOF after array element")
628 | }
629 | if b[0] != ',' {
630 | return nil, b, syntaxError(b, "expected ',' after array element but found '%c'", b[0])
631 | }
632 | b = skipSpaces(b[1:])
633 | if len(b) == 0 {
634 | return nil, b, unexpectedEOF(b)
635 | }
636 | if b[0] == ']' {
637 | return nil, b, syntaxError(b, "unexpected trailing comma after object field")
638 | }
639 | }
640 |
641 | _, b, err = parseValue(b)
642 | if err != nil {
643 | return nil, b, err
644 | }
645 |
646 | i++
647 | }
648 | }
649 |
650 | func parseValue(b []byte) ([]byte, []byte, error) {
651 | if len(b) != 0 {
652 | switch b[0] {
653 | case '{':
654 | return parseObject(b)
655 | case '[':
656 | return parseArray(b)
657 | case '"':
658 | return parseString(b)
659 | case 'n':
660 | return parseNull(b)
661 | case 't':
662 | return parseTrue(b)
663 | case 'f':
664 | return parseFalse(b)
665 | case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
666 | return parseNumber(b)
667 | default:
668 | return nil, b, syntaxError(b, "invalid character '%c' looking for beginning of value", b[0])
669 | }
670 | }
671 | return nil, b, syntaxError(b, "unexpected end of JSON input")
672 | }
673 |
674 | func hasNullPrefix(b []byte) bool {
675 | return len(b) >= 4 && string(b[:4]) == "null"
676 | }
677 |
678 | func hasTruePrefix(b []byte) bool {
679 | return len(b) >= 4 && string(b[:4]) == "true"
680 | }
681 |
682 | func hasFalsePrefix(b []byte) bool {
683 | return len(b) >= 5 && string(b[:5]) == "false"
684 | }
685 |
686 | func hasPrefix(b []byte, s string) bool {
687 | return len(b) >= len(s) && s == string(b[:len(s)])
688 | }
689 |
690 | func hasLeadingSign(b []byte) bool {
691 | return len(b) > 0 && (b[0] == '+' || b[0] == '-')
692 | }
693 |
694 | func hasLeadingZeroes(b []byte) bool {
695 | if hasLeadingSign(b) {
696 | b = b[1:]
697 | }
698 | return len(b) > 1 && b[0] == '0' && '0' <= b[1] && b[1] <= '9'
699 | }
700 |
701 | func appendToLower(b, s []byte) []byte {
702 | if asciiValid(s) { // fast path for ascii strings
703 | i := 0
704 |
705 | for j := range s {
706 | c := s[j]
707 |
708 | if 'A' <= c && c <= 'Z' {
709 | b = append(b, s[i:j]...)
710 | b = append(b, c+('a'-'A'))
711 | i = j + 1
712 | }
713 | }
714 |
715 | return append(b, s[i:]...)
716 | }
717 |
718 | for _, r := range string(s) {
719 | b = appendRune(b, foldRune(r))
720 | }
721 |
722 | return b
723 | }
724 |
725 | func foldRune(r rune) rune {
726 | if r = unicode.SimpleFold(r); 'A' <= r && r <= 'Z' {
727 | r += 'a' - 'A'
728 | }
729 | return r
730 | }
731 |
--------------------------------------------------------------------------------
/parse_test.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import (
4 | "bytes"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestParseString(t *testing.T) {
10 | tests := []struct {
11 | in string
12 | out string
13 | ext string
14 | }{
15 | {`""`, `""`, ``},
16 | {`"1234567890"`, `"1234567890"`, ``},
17 | {`"Hello World!"`, `"Hello World!"`, ``},
18 | {`"Hello\"World!"`, `"Hello\"World!"`, ``},
19 | {`"\\"`, `"\\"`, ``},
20 | }
21 |
22 | for _, test := range tests {
23 | t.Run(test.in, func(t *testing.T) {
24 | out, ext, err := parseString([]byte(test.in))
25 | if err != nil {
26 | t.Errorf("%s => %s", test.in, err)
27 | return
28 | }
29 |
30 | if s := string(out); s != test.out {
31 | t.Error("invalid output")
32 | t.Logf("expected: %s", test.out)
33 | t.Logf("found: %s", s)
34 | }
35 |
36 | if s := string(ext); s != test.ext {
37 | t.Error("invalid extra bytes")
38 | t.Logf("expected: %s", test.ext)
39 | t.Logf("found: %s", s)
40 | }
41 | })
42 | }
43 | }
44 |
45 | func TestAppendToLower(t *testing.T) {
46 | tests := []string{
47 | "",
48 | "A",
49 | "a",
50 | "__segment_internal",
51 | "someFieldWithALongBName",
52 | "Hello World!",
53 | "Hello\"World!",
54 | "Hello\\World!",
55 | "Hello\nWorld!",
56 | "Hello\rWorld!",
57 | "Hello\tWorld!",
58 | "Hello\bWorld!",
59 | "Hello\fWorld!",
60 | "你好",
61 | "<",
62 | ">",
63 | "&",
64 | "\u001944",
65 | "\u00c2e>",
66 | "\u00c2V?",
67 | "\u000e=8",
68 | "\u001944\u00c2e>\u00c2V?\u000e=8",
69 | "ir\u001bQJ\u007f\u0007y\u0015)",
70 | }
71 |
72 | for _, test := range tests {
73 | s1 := strings.ToLower(test)
74 | s2 := string(appendToLower(nil, []byte(test)))
75 |
76 | if s1 != s2 {
77 | t.Error("lowercase values mismatch")
78 | t.Log("expected:", s1)
79 | t.Log("found: ", s2)
80 | }
81 | }
82 | }
83 |
84 | func BenchmarkParseString(b *testing.B) {
85 | s := []byte(`"__segment_internal"`)
86 |
87 | for i := 0; i != b.N; i++ {
88 | parseString(s)
89 | }
90 | }
91 |
92 | func BenchmarkToLower(b *testing.B) {
93 | s := []byte("someFieldWithALongName")
94 |
95 | for i := 0; i != b.N; i++ {
96 | bytes.ToLower(s)
97 | }
98 | }
99 |
100 | func BenchmarkAppendToLower(b *testing.B) {
101 | a := []byte(nil)
102 | s := []byte("someFieldWithALongName")
103 |
104 | for i := 0; i != b.N; i++ {
105 | a = appendToLower(a[:0], s)
106 | }
107 | }
108 |
109 | var (
110 | benchmarkHasPrefixString = []byte("some random string")
111 | benchmarkHasPrefixResult = false
112 | )
113 |
114 | func BenchmarkHasPrefix(b *testing.B) {
115 | for i := 0; i < b.N; i++ {
116 | benchmarkHasPrefixResult = hasPrefix(benchmarkHasPrefixString, "null")
117 | }
118 | }
119 |
120 | func BenchmarkHasNullPrefix(b *testing.B) {
121 | for i := 0; i < b.N; i++ {
122 | benchmarkHasPrefixResult = hasNullPrefix(benchmarkHasPrefixString)
123 | }
124 | }
125 |
126 | func BenchmarkHasTruePrefix(b *testing.B) {
127 | for i := 0; i < b.N; i++ {
128 | benchmarkHasPrefixResult = hasTruePrefix(benchmarkHasPrefixString)
129 | }
130 | }
131 |
132 | func BenchmarkHasFalsePrefix(b *testing.B) {
133 | for i := 0; i < b.N; i++ {
134 | benchmarkHasPrefixResult = hasFalsePrefix(benchmarkHasPrefixString)
135 | }
136 | }
137 |
138 | func BenchmarkParseStringEscapeNone(b *testing.B) {
139 | j := []byte(`"` + strings.Repeat(`a`, 1000) + `"`)
140 | var s string
141 | b.SetBytes(int64(len(j)))
142 |
143 | for i := 0; i < b.N; i++ {
144 | if err := Unmarshal(j, &s); err != nil {
145 | b.Fatal(err)
146 | }
147 | s = ""
148 | }
149 | }
150 |
151 | func BenchmarkParseStringEscapeOne(b *testing.B) {
152 | j := []byte(`"` + strings.Repeat(`a`, 998) + `\n"`)
153 | var s string
154 | b.SetBytes(int64(len(j)))
155 |
156 | for i := 0; i < b.N; i++ {
157 | if err := Unmarshal(j, &s); err != nil {
158 | b.Fatal(err)
159 | }
160 | s = ""
161 | }
162 | }
163 |
164 | func BenchmarkParseStringEscapeAll(b *testing.B) {
165 | j := []byte(`"` + strings.Repeat(`\`, 1000) + `"`)
166 | var s string
167 | b.SetBytes(int64(len(j)))
168 |
169 | for i := 0; i < b.N; i++ {
170 | if err := Unmarshal(j, &s); err != nil {
171 | b.Fatal(err)
172 | }
173 | s = ""
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/reflect.go:
--------------------------------------------------------------------------------
1 | //go:build go1.15
2 | // +build go1.15
3 |
4 | package jsoncolor
5 |
6 | import (
7 | "reflect"
8 | "unsafe"
9 | )
10 |
11 | func extendSlice(t reflect.Type, s *slice, n int) slice {
12 | arrayType := reflect.ArrayOf(n, t.Elem())
13 | arrayData := reflect.New(arrayType)
14 | reflect.Copy(arrayData.Elem(), reflect.NewAt(t, unsafe.Pointer(s)).Elem())
15 | return slice{
16 | data: unsafe.Pointer(arrayData.Pointer()),
17 | len: s.len,
18 | cap: n,
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/reflect_optimize.go:
--------------------------------------------------------------------------------
1 | //go:build !go1.15
2 | // +build !go1.15
3 |
4 | package jsoncolor
5 |
6 | import (
7 | "reflect"
8 | "unsafe"
9 | )
10 |
11 | //go:linkname unsafe_NewArray reflect.unsafe_NewArray
12 | func unsafe_NewArray(rtype unsafe.Pointer, length int) unsafe.Pointer
13 |
14 | //go:linkname typedslicecopy reflect.typedslicecopy
15 | //go:noescape
16 | func typedslicecopy(elemType unsafe.Pointer, dst, src slice) int
17 |
18 | func extendSlice(t reflect.Type, s *slice, n int) slice {
19 | elemTypeRef := t.Elem()
20 | elemTypePtr := ((*iface)(unsafe.Pointer(&elemTypeRef))).ptr
21 |
22 | d := slice{
23 | data: unsafe_NewArray(elemTypePtr, n),
24 | len: s.len,
25 | cap: n,
26 | }
27 |
28 | typedslicecopy(elemTypePtr, d, *s)
29 | return d
30 | }
31 |
--------------------------------------------------------------------------------
/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neilotoole/jsoncolor/1637fae69be1e115df81c7015d6847675b4cf8d4/splash.png
--------------------------------------------------------------------------------
/terminal.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package jsoncolor
4 |
5 | import (
6 | "io"
7 | "os"
8 |
9 | "golang.org/x/term"
10 | )
11 |
12 | // IsColorTerminal returns true if w is a colorable terminal.
13 | // It respects [NO_COLOR], [FORCE_COLOR] and TERM=dumb environment variables.
14 | //
15 | // [NO_COLOR]: https://no-color.org/
16 | // [FORCE_COLOR]: https://force-color.org/
17 | func IsColorTerminal(w io.Writer) bool {
18 | if os.Getenv("NO_COLOR") != "" {
19 | return false
20 | }
21 | if os.Getenv("FORCE_COLOR") != "" {
22 | return true
23 | }
24 | if os.Getenv("TERM") == "dumb" {
25 | return false
26 | }
27 |
28 | if w == nil {
29 | return false
30 | }
31 |
32 | f, ok := w.(*os.File)
33 | if !ok {
34 | return false
35 | }
36 |
37 | if !term.IsTerminal(int(f.Fd())) {
38 | return false
39 | }
40 |
41 | return true
42 | }
43 |
--------------------------------------------------------------------------------
/terminal_windows.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import (
4 | "io"
5 | "os"
6 |
7 | "golang.org/x/sys/windows"
8 | )
9 |
10 | // IsColorTerminal returns true if w is a colorable terminal.
11 | // It respects [NO_COLOR], [FORCE_COLOR] and TERM=dumb environment variables.
12 | //
13 | // [NO_COLOR]: https://no-color.org/
14 | // [FORCE_COLOR]: https://force-color.org/
15 | func IsColorTerminal(w io.Writer) bool {
16 | if os.Getenv("NO_COLOR") != "" {
17 | return false
18 | }
19 | if os.Getenv("FORCE_COLOR") != "" {
20 | return true
21 | }
22 | if os.Getenv("TERM") == "dumb" {
23 | return false
24 | }
25 |
26 | if w == nil {
27 | return false
28 | }
29 |
30 | f, ok := w.(*os.File)
31 | if !ok {
32 | return false
33 | }
34 | fd := f.Fd()
35 |
36 | console := windows.Handle(fd)
37 | var mode uint32
38 | if err := windows.GetConsoleMode(console, &mode); err != nil {
39 | return false
40 | }
41 |
42 | var want uint32 = windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
43 | if (mode & want) == want {
44 | return true
45 | }
46 |
47 | mode |= want
48 | if err := windows.SetConsoleMode(console, mode); err != nil {
49 | return false
50 | }
51 |
52 | return true
53 | }
54 |
--------------------------------------------------------------------------------
/testdata/code.json.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neilotoole/jsoncolor/1637fae69be1e115df81c7015d6847675b4cf8d4/testdata/code.json.gz
--------------------------------------------------------------------------------
/testdata/example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "actor_id": 1,
4 | "first_name": "PENELOPE",
5 | "last_name": "GUINESS",
6 | "last_update": "2020-06-11T02:50:54Z"
7 | },
8 | {
9 | "actor_id": 2,
10 | "first_name": "NICK",
11 | "last_name": "WAHLBERG",
12 | "last_update": "2020-06-11T02:50:54Z"
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/testdata/msgs.json.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neilotoole/jsoncolor/1637fae69be1e115df81c7015d6847675b4cf8d4/testdata/msgs.json.gz
--------------------------------------------------------------------------------
/token.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | // Tokenizer is an iterator-style type which can be used to progressively parse
4 | // through a json input.
5 | //
6 | // Tokenizing json is useful to build highly efficient parsing operations, for
7 | // example when doing tranformations on-the-fly where as the program reads the
8 | // input and produces the transformed json to an output buffer.
9 | //
10 | // Here is a common pattern to use a tokenizer:
11 | //
12 | // for t := json.NewTokenizer(b); t.Next(); {
13 | // switch t.Delim {
14 | // case '{':
15 | // ...
16 | // case '}':
17 | // ...
18 | // case '[':
19 | // ...
20 | // case ']':
21 | // ...
22 | // case ':':
23 | // ...
24 | // case ',':
25 | // ...
26 | // }
27 | //
28 | // switch {
29 | // case t.Value.String():
30 | // ...
31 | // case t.Value.Null():
32 | // ...
33 | // case t.Value.True():
34 | // ...
35 | // case t.Value.False():
36 | // ...
37 | // case t.Value.Number():
38 | // ...
39 | // }
40 | // }
41 | type Tokenizer struct {
42 | // When the tokenizer is positioned on a json delimiter this field is not
43 | // zero. In this case the possible values are '{', '}', '[', ']', ':', and
44 | // ','.
45 | Delim Delim
46 |
47 | // This field contains the raw json token that the tokenizer is pointing at.
48 | // When Delim is not zero, this field is a single-element byte slice
49 | // continaing the delimiter value. Otherwise, this field holds values like
50 | // null, true, false, numbers, or quoted strings.
51 | Value RawValue
52 |
53 | // When the tokenizer has encountered invalid content this field is not nil.
54 | Err error
55 |
56 | // When the value is in an array or an object, this field contains the depth
57 | // at which it was found.
58 | Depth int
59 |
60 | // When the value is in an array or an object, this field contains the
61 | // position at which it was found.
62 | Index int
63 |
64 | // This field is true when the value is the key of an object.
65 | IsKey bool
66 |
67 | // Tells whether the next value read from the tokenizer is a key.
68 | isKey bool
69 |
70 | // json input for the tokenizer, pointing at data right after the last token
71 | // that was parsed.
72 | json []byte
73 |
74 | // Stack used to track entering and leaving arrays, objects, and keys. The
75 | // buffer is used as a AppendPre-allocated space to
76 | stack []state
77 | buffer [8]state
78 | }
79 |
80 | type state struct {
81 | typ scope
82 | len int
83 | }
84 |
85 | type scope int
86 |
87 | const (
88 | inArray scope = iota
89 | inObject
90 | )
91 |
92 | // NewTokenizer constructs a new Tokenizer which reads its json input from b.
93 | func NewTokenizer(b []byte) *Tokenizer { return &Tokenizer{json: b} }
94 |
95 | // Reset erases the state of t and re-initializes it with the json input from b.
96 | func (t *Tokenizer) Reset(b []byte) {
97 | // This code is similar to:
98 | //
99 | // *t = Tokenizer{json: b}
100 | //
101 | // However, it does not compile down to an invocation of duff-copy, which
102 | // ends up being slower and prevents the code from being inlined.
103 | t.Delim = 0
104 | t.Value = nil
105 | t.Err = nil
106 | t.Depth = 0
107 | t.Index = 0
108 | t.IsKey = false
109 | t.isKey = false
110 | t.json = b
111 | t.stack = nil
112 | }
113 |
114 | // Next returns a new tokenizer pointing at the next token, or the zero-value of
115 | // Tokenizer if the end of the json input has been reached.
116 | //
117 | // If the tokenizer encounters malformed json while reading the input the method
118 | // sets t.Err to an error describing the issue, and returns false. Once an error
119 | // has been encountered, the tokenizer will always fail until its input is
120 | // cleared by a call to its Reset method.
121 | func (t *Tokenizer) Next() bool {
122 | if t.Err != nil {
123 | return false
124 | }
125 |
126 | // Inlined code of the skipSpaces function, this give a ~15% speed boost.
127 | i := 0
128 | skipLoop:
129 | for _, c := range t.json {
130 | switch c {
131 | case sp, ht, nl, cr:
132 | i++
133 | default:
134 | break skipLoop
135 | }
136 | }
137 |
138 | if t.json = t.json[i:]; len(t.json) == 0 {
139 | t.Reset(nil)
140 | return false
141 | }
142 |
143 | var d Delim
144 | var v []byte
145 | var b []byte
146 | var err error
147 |
148 | switch t.json[0] {
149 | case '"':
150 | v, b, err = parseString(t.json)
151 | case 'n':
152 | v, b, err = parseNull(t.json)
153 | case 't':
154 | v, b, err = parseTrue(t.json)
155 | case 'f':
156 | v, b, err = parseFalse(t.json)
157 | case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
158 | v, b, err = parseNumber(t.json)
159 | case '{', '}', '[', ']', ':', ',':
160 | d, v, b = Delim(t.json[0]), t.json[:1], t.json[1:]
161 | default:
162 | v, b, err = t.json[:1], t.json[1:], syntaxError(t.json, "expected token but found '%c'", t.json[0])
163 | }
164 |
165 | t.Delim = d
166 | t.Value = RawValue(v)
167 | t.Err = err
168 | t.Depth = t.depth()
169 | t.Index = t.index()
170 | t.IsKey = d == 0 && t.isKey
171 | t.json = b
172 |
173 | if d != 0 {
174 | switch d {
175 | case '{':
176 | t.isKey = true
177 | t.push(inObject)
178 | case '[':
179 | t.push(inArray)
180 | case '}':
181 | err = t.pop(inObject)
182 | t.Depth--
183 | t.Index = t.index()
184 | case ']':
185 | err = t.pop(inArray)
186 | t.Depth--
187 | t.Index = t.index()
188 | case ':':
189 | t.isKey = false
190 | case ',':
191 | if t.is(inObject) {
192 | t.isKey = true
193 | }
194 | t.stack[len(t.stack)-1].len++
195 | }
196 | }
197 |
198 | return (d != 0 || len(v) != 0) && err == nil
199 | }
200 |
201 | func (t *Tokenizer) push(typ scope) {
202 | if t.stack == nil {
203 | t.stack = t.buffer[:0]
204 | }
205 | t.stack = append(t.stack, state{typ: typ, len: 1})
206 | }
207 |
208 | func (t *Tokenizer) pop(expect scope) error {
209 | i := len(t.stack) - 1
210 |
211 | if i < 0 {
212 | return syntaxError(t.json, "found unexpected character while tokenizing json input")
213 | }
214 |
215 | if found := t.stack[i]; expect != found.typ {
216 | return syntaxError(t.json, "found unexpected character while tokenizing json input")
217 | }
218 |
219 | t.stack = t.stack[:i]
220 | return nil
221 | }
222 |
223 | func (t *Tokenizer) is(typ scope) bool {
224 | return len(t.stack) != 0 && t.stack[len(t.stack)-1].typ == typ
225 | }
226 |
227 | func (t *Tokenizer) depth() int {
228 | return len(t.stack)
229 | }
230 |
231 | func (t *Tokenizer) index() int {
232 | if len(t.stack) == 0 {
233 | return 0
234 | }
235 | return t.stack[len(t.stack)-1].len - 1
236 | }
237 |
238 | // RawValue represents a raw json value, it is intended to carry null, true,
239 | // false, number, and string values only.
240 | type RawValue []byte
241 |
242 | // String returns true if v contains a string value.
243 | func (v RawValue) String() bool { return len(v) != 0 && v[0] == '"' }
244 |
245 | // Null returns true if v contains a null value.
246 | func (v RawValue) Null() bool { return len(v) != 0 && v[0] == 'n' }
247 |
248 | // True returns true if v contains a true value.
249 | func (v RawValue) True() bool { return len(v) != 0 && v[0] == 't' }
250 |
251 | // False returns true if v contains a false value.
252 | func (v RawValue) False() bool { return len(v) != 0 && v[0] == 'f' }
253 |
254 | // Number returns true if v contains a number value.
255 | func (v RawValue) Number() bool {
256 | if len(v) != 0 {
257 | switch v[0] {
258 | case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
259 | return true
260 | }
261 | }
262 | return false
263 | }
264 |
265 | // AppendUnquote writes the unquoted version of the string value in v into b.
266 | func (v RawValue) AppendUnquote(b []byte) []byte {
267 | s, r, isNew, err := parseStringUnquote([]byte(v), b)
268 | if err != nil {
269 | panic(err)
270 | }
271 | if len(r) != 0 {
272 | panic(syntaxError(r, "unexpected trailing tokens after json value"))
273 | }
274 | if isNew {
275 | b = s
276 | } else {
277 | b = append(b, s...)
278 | }
279 | return b
280 | }
281 |
282 | // Unquote returns the unquoted version of the string value in v.
283 | func (v RawValue) Unquote() []byte {
284 | return v.AppendUnquote(nil)
285 | }
286 |
--------------------------------------------------------------------------------
/token_test.go:
--------------------------------------------------------------------------------
1 | package jsoncolor
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | type token struct {
9 | delim Delim
10 | value RawValue
11 | err error
12 | depth int
13 | index int
14 | isKey bool
15 | }
16 |
17 | func delim(s string, depth, index int) token {
18 | return token{
19 | delim: Delim(s[0]),
20 | value: RawValue(s),
21 | depth: depth,
22 | index: index,
23 | }
24 | }
25 |
26 | func key(v string, depth, index int) token {
27 | return token{
28 | value: RawValue(v),
29 | depth: depth,
30 | index: index,
31 | isKey: true,
32 | }
33 | }
34 |
35 | func value(v string, depth, index int) token {
36 | return token{
37 | value: RawValue(v),
38 | depth: depth,
39 | index: index,
40 | }
41 | }
42 |
43 | func tokenize(b []byte) (tokens []token) {
44 | t := NewTokenizer(b)
45 |
46 | for t.Next() {
47 | tokens = append(tokens, token{
48 | delim: t.Delim,
49 | value: t.Value,
50 | err: t.Err,
51 | depth: t.Depth,
52 | index: t.Index,
53 | isKey: t.IsKey,
54 | })
55 | }
56 |
57 | if t.Err != nil {
58 | panic(t.Err)
59 | }
60 |
61 | return
62 | }
63 |
64 | func TestTokenizer(t *testing.T) {
65 | tests := []struct {
66 | input []byte
67 | tokens []token
68 | }{
69 | {
70 | input: []byte(`null`),
71 | tokens: []token{
72 | value(`null`, 0, 0),
73 | },
74 | },
75 |
76 | {
77 | input: []byte(`true`),
78 | tokens: []token{
79 | value(`true`, 0, 0),
80 | },
81 | },
82 |
83 | {
84 | input: []byte(`false`),
85 | tokens: []token{
86 | value(`false`, 0, 0),
87 | },
88 | },
89 |
90 | {
91 | input: []byte(`""`),
92 | tokens: []token{
93 | value(`""`, 0, 0),
94 | },
95 | },
96 |
97 | {
98 | input: []byte(`"Hello World!"`),
99 | tokens: []token{
100 | value(`"Hello World!"`, 0, 0),
101 | },
102 | },
103 |
104 | {
105 | input: []byte(`-0.1234`),
106 | tokens: []token{
107 | value(`-0.1234`, 0, 0),
108 | },
109 | },
110 |
111 | {
112 | input: []byte(` { } `),
113 | tokens: []token{
114 | delim(`{`, 0, 0),
115 | delim(`}`, 0, 0),
116 | },
117 | },
118 |
119 | {
120 | input: []byte(`{ "answer": 42 }`),
121 | tokens: []token{
122 | delim(`{`, 0, 0),
123 | key(`"answer"`, 1, 0),
124 | delim(`:`, 1, 0),
125 | value(`42`, 1, 0),
126 | delim(`}`, 0, 0),
127 | },
128 | },
129 |
130 | {
131 | input: []byte(`{ "sub": { "key-A": 1, "key-B": 2, "key-C": 3 } }`),
132 | tokens: []token{
133 | delim(`{`, 0, 0),
134 | key(`"sub"`, 1, 0),
135 | delim(`:`, 1, 0),
136 | delim(`{`, 1, 0),
137 | key(`"key-A"`, 2, 0),
138 | delim(`:`, 2, 0),
139 | value(`1`, 2, 0),
140 | delim(`,`, 2, 0),
141 | key(`"key-B"`, 2, 1),
142 | delim(`:`, 2, 1),
143 | value(`2`, 2, 1),
144 | delim(`,`, 2, 1),
145 | key(`"key-C"`, 2, 2),
146 | delim(`:`, 2, 2),
147 | value(`3`, 2, 2),
148 | delim(`}`, 1, 0),
149 | delim(`}`, 0, 0),
150 | },
151 | },
152 |
153 | {
154 | input: []byte(` [ ] `),
155 | tokens: []token{
156 | delim(`[`, 0, 0),
157 | delim(`]`, 0, 0),
158 | },
159 | },
160 |
161 | {
162 | input: []byte(`[1, 2, 3]`),
163 | tokens: []token{
164 | delim(`[`, 0, 0),
165 | value(`1`, 1, 0),
166 | delim(`,`, 1, 0),
167 | value(`2`, 1, 1),
168 | delim(`,`, 1, 1),
169 | value(`3`, 1, 2),
170 | delim(`]`, 0, 0),
171 | },
172 | },
173 | }
174 |
175 | for _, test := range tests {
176 | t.Run(string(test.input), func(t *testing.T) {
177 | tokens := tokenize(test.input)
178 |
179 | if !reflect.DeepEqual(tokens, test.tokens) {
180 | t.Error("tokens mismatch")
181 | t.Logf("expected: %+v", test.tokens)
182 | t.Logf("found: %+v", tokens)
183 | }
184 | })
185 | }
186 | }
187 |
188 | func BenchmarkTokenizer(b *testing.B) {
189 | values := []struct {
190 | scenario string
191 | payload []byte
192 | }{
193 | {
194 | scenario: "null",
195 | payload: []byte(`null`),
196 | },
197 |
198 | {
199 | scenario: "true",
200 | payload: []byte(`true`),
201 | },
202 |
203 | {
204 | scenario: "false",
205 | payload: []byte(`false`),
206 | },
207 |
208 | {
209 | scenario: "number",
210 | payload: []byte(`-1.23456789`),
211 | },
212 |
213 | {
214 | scenario: "string",
215 | payload: []byte(`"1234567890"`),
216 | },
217 |
218 | {
219 | scenario: "object",
220 | payload: []byte(`{
221 | "timestamp": "2019-01-09T18:59:57.456Z",
222 | "channel": "server",
223 | "type": "track",
224 | "event": "Test",
225 | "userId": "test-user-whatever",
226 | "messageId": "test-message-whatever",
227 | "integrations": {
228 | "whatever": {
229 | "debugMode": false
230 | },
231 | "myIntegration": {
232 | "debugMode": true
233 | }
234 | },
235 | "properties": {
236 | "trait1": 1,
237 | "trait2": "test",
238 | "trait3": true
239 | },
240 | "settings": {
241 | "apiKey": "1234567890",
242 | "debugMode": false,
243 | "directChannels": [
244 | "server",
245 | "client"
246 | ],
247 | "endpoint": "https://somewhere.com/v1/integrations/segment"
248 | }
249 | }`),
250 | },
251 | }
252 |
253 | benchmarks := []struct {
254 | scenario string
255 | function func(*testing.B, []byte)
256 | }{
257 | {
258 | scenario: "github.com/segmentio/encoding/json",
259 | function: func(b *testing.B, json []byte) {
260 | t := NewTokenizer(nil)
261 |
262 | for i := 0; i < b.N; i++ {
263 | t.Reset(json)
264 |
265 | for t.Next() {
266 | // Does nothing other than iterating over each token to measure the
267 | // CPU and memory footprint.
268 | }
269 |
270 | if t.Err != nil {
271 | b.Error(t.Err)
272 | }
273 | }
274 | },
275 | },
276 | }
277 |
278 | for _, bechmark := range benchmarks {
279 | b.Run(bechmark.scenario, func(b *testing.B) {
280 | for _, value := range values {
281 | b.Run(value.scenario, func(b *testing.B) {
282 | bechmark.function(b, value.payload)
283 | })
284 | }
285 | })
286 | }
287 | }
288 |
--------------------------------------------------------------------------------