├── .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 | [![Actions Status](https://github.com/neilotoole/jsoncolor/workflows/Go/badge.svg)](https://github.com/neilotoole/jsoncolor/actions?query=workflow%3AGo) 2 | [![Go Report Card](https://goreportcard.com/badge/neilotoole/jsoncolor)](https://goreportcard.com/report/neilotoole/jsoncolor) 3 | [![release](https://img.shields.io/badge/release-v0.7.0-green.svg)](https://github.com/neilotoole/jsoncolor/releases/tag/v0.7.0) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/neilotoole/jsoncolor.svg)](https://pkg.go.dev/github.com/neilotoole/jsoncolor) 5 | [![license](https://img.shields.io/github/license/neilotoole/jsoncolor)](./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 | ![jsoncolor-output](./splash.png) 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 [![GoDoc](https://godoc.org/github.com/segmentio/encoding/json?status.svg)](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 | --------------------------------------------------------------------------------