├── images ├── logo.png └── benchmarks │ ├── simple.svg │ ├── complex.svg │ ├── code-marshal.svg │ └── map.svg ├── testdata └── code.json.gz ├── .codecov.yml ├── .goreleaser.yml ├── tools ├── charts │ ├── jsdom.html │ ├── .eslintrc.js │ ├── package.json │ └── index.js └── benchparse │ └── benchparse.go ├── ci ├── test.sh └── bench.sh ├── map_1.17.go ├── map_1.18.go ├── go.mod ├── buffer.go ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── .gitignore ├── tags.go ├── tags_test.go ├── .revive.toml ├── LICENSE ├── number.go ├── LICENSE.golang ├── unsafe.go ├── map.go ├── number_test.go ├── integer.go ├── go.sum ├── json_1.14_test.go ├── json_1.13_test.go ├── time_test.go ├── CHANGELOG.md ├── types.go ├── json.go ├── time.go ├── bench_test.go ├── options.go ├── struct.go ├── example_test.go ├── instruction.go ├── README.md ├── encode.go └── json_test.go /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wI2L/jettison/HEAD/images/logo.png -------------------------------------------------------------------------------- /testdata/code.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wI2L/jettison/HEAD/testdata/code.json.gz -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 70..90 3 | status: 4 | # Prevent small variations of coverage from failing CI. 5 | project: 6 | default: 7 | threshold: 1% 8 | patch: off 9 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: jettison 2 | builds: 3 | - skip: true 4 | release: 5 | github: 6 | owner: wI2L 7 | name: jettison 8 | draft: true 9 | prerelease: auto 10 | env_files: 11 | github_token: ~/.goreleaser_github_token 12 | -------------------------------------------------------------------------------- /tools/charts/jsdom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v vendor); do 7 | go test -v -race -coverprofile=profile.out -covermode=atomic "$d" 8 | if [ -f profile.out ]; then 9 | cat profile.out >> coverage.txt 10 | rm profile.out 11 | fi 12 | done 13 | -------------------------------------------------------------------------------- /tools/charts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'standard' 9 | ], 10 | globals: { 11 | Atomics: 'readonly', 12 | SharedArrayBuffer: 'readonly' 13 | }, 14 | parserOptions: { 15 | ecmaVersion: 2018 16 | }, 17 | rules: { 18 | 'indent': ['error', 4] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /map_1.17.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.18 2 | 3 | package jettison 4 | 5 | import "unsafe" 6 | 7 | func newHiter(t, m unsafe.Pointer) *hiter { 8 | v := hiterPool.Get() 9 | if v == nil { 10 | return newmapiter(t, m) 11 | } 12 | it := v.(*hiter) 13 | *it = *zeroHiter 14 | mapiterinit(t, m, unsafe.Pointer(it)) 15 | return it 16 | } 17 | 18 | //go:noescape 19 | //go:linkname newmapiter reflect.mapiterinit 20 | func newmapiter(unsafe.Pointer, unsafe.Pointer) *hiter 21 | -------------------------------------------------------------------------------- /map_1.18.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | 3 | package jettison 4 | 5 | import "unsafe" 6 | 7 | func newHiter(t, m unsafe.Pointer) *hiter { 8 | v := hiterPool.Get() 9 | if v == nil { 10 | var it hiter 11 | newmapiter(t, m, &it) 12 | return &it 13 | } 14 | it := v.(*hiter) 15 | *it = *zeroHiter 16 | mapiterinit(t, m, unsafe.Pointer(it)) 17 | return it 18 | } 19 | 20 | //go:noescape 21 | //go:linkname newmapiter reflect.mapiterinit 22 | func newmapiter(unsafe.Pointer, unsafe.Pointer, *hiter) 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wI2L/jettison 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/json-iterator/go v1.1.12 7 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 8 | github.com/modern-go/reflect2 v1.0.2 // indirect 9 | github.com/segmentio/encoding v0.3.4 10 | github.com/stretchr/testify v1.5.1 // indirect 11 | gopkg.in/yaml.v2 v2.2.8 // indirect 12 | ) 13 | 14 | require ( 15 | github.com/segmentio/asm v1.1.3 // indirect 16 | golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /buffer.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import "sync" 4 | 5 | const defaultBufCap = 4096 6 | 7 | var bufferPool sync.Pool // *buffer 8 | 9 | type buffer struct{ B []byte } 10 | 11 | // Reset resets the buffer to be empty. 12 | func (b *buffer) Reset() { b.B = b.B[:0] } 13 | 14 | // cachedBuffer returns an empty buffer 15 | // from a pool, or initialize a new one 16 | // with a default capacity. 17 | func cachedBuffer() *buffer { 18 | v := bufferPool.Get() 19 | if v != nil { 20 | buf := v.(*buffer) 21 | buf.Reset() 22 | return buf 23 | } 24 | return &buffer{ 25 | B: make([]byte, 0, defaultBufCap), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.17 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v2 25 | with: 26 | distribution: goreleaser 27 | version: latest 28 | args: release --rm-dist 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Linux 2 | *~ 3 | .Trash-* 4 | 5 | # Mac OSX 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | ._* 10 | 11 | # Folders 12 | _obj 13 | _test 14 | 15 | # Compiled Object files 16 | # Static and Dynamic libs (Shared Objects) 17 | *.o 18 | *.a 19 | *.so 20 | *.dll 21 | *.dylib 22 | 23 | # Executables 24 | *.exe 25 | *.exe~ 26 | 27 | # Architecture specific extensions/prefixes 28 | *.[568vq] 29 | [568vq].out 30 | 31 | # Cgo 32 | *.cgo1.go 33 | *.cgo2.c 34 | _cgo_defun.c 35 | _cgo_gotypes.go 36 | _cgo_export.* 37 | 38 | # Go Tools 39 | *.test 40 | *.prof 41 | *.out 42 | _testmain.go 43 | 44 | # Dependency directories 45 | vendor/ 46 | Godeps/ 47 | 48 | # CI/CD 49 | coverage.txt 50 | benchstats 51 | .benchruns 52 | .benchstats.csv 53 | .benchstats.json 54 | 55 | # NodeJS 56 | node_modules 57 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import "strings" 4 | 5 | // tagOptions represents the arguments following 6 | // a comma in a struct field's tag. 7 | type tagOptions []string 8 | 9 | // parseTag parses the content of a struct field 10 | // tag and return the name and list of options. 11 | func parseTag(tag string) (string, tagOptions) { 12 | if idx := strings.Index(tag, ","); idx != -1 { 13 | return tag[:idx], strings.Split(tag[idx+1:], ",") 14 | } 15 | return tag, nil 16 | } 17 | 18 | // Contains returns whether a list of options 19 | // contains a particular substring flag. 20 | func (opts tagOptions) Contains(name string) bool { 21 | if len(opts) == 0 { 22 | return false 23 | } 24 | for _, o := range opts { 25 | if o == name { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | -------------------------------------------------------------------------------- /tags_test.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import "testing" 4 | 5 | func TestParseTag(t *testing.T) { 6 | testdata := []struct { 7 | tag string 8 | name string 9 | opts []string 10 | }{ 11 | {"", "", nil}, 12 | {"foobar", "foobar", nil}, 13 | {"foo,bar", "foo", []string{"bar"}}, 14 | {"bar,", "bar", nil}, 15 | {"a,b,c,", "a", []string{"b", "c"}}, 16 | {" foo , bar ,", " foo ", []string{" bar "}}, 17 | {",bar", "", []string{"bar"}}, 18 | {", ", "", []string{" "}}, 19 | {",", "", nil}, 20 | {"bar, ,foo", "bar", []string{" ", "foo"}}, 21 | } 22 | for _, v := range testdata { 23 | name, opts := parseTag(v.tag) 24 | if name != v.name { 25 | t.Errorf("tag name: got %q, want %q", name, v.name) 26 | } 27 | for _, opt := range v.opts { 28 | if !opts.Contains(opt) { 29 | t.Errorf("missing tag option %q", opt) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tools/charts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jettison-charts", 3 | "version": "0.2.0", 4 | "description": "Google Charts generator for Jettison benchmarks", 5 | "main": "index.js", 6 | "author": "William Poussier", 7 | "license": "MIT", 8 | "dependencies": { 9 | "canvas": "^2.6.1", 10 | "commander": "^3.0.2", 11 | "decamelize": "^3.2.0", 12 | "jsdom": "^15.2.1", 13 | "svgo": "^2.7.0" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^8.0.1", 17 | "eslint-config-standard": "^14.1.1", 18 | "eslint-plugin-import": "^2.22.0", 19 | "eslint-plugin-node": "^10.0.0", 20 | "eslint-plugin-promise": "^4.2.1", 21 | "eslint-plugin-standard": "^4.0.1", 22 | "standard": "^16.0.4" 23 | }, 24 | "scripts": { 25 | "test": "npx standard", 26 | "preinstall": "npx npm-force-resolutions" 27 | }, 28 | "resolutions": { 29 | "ansi-regex": "5.0.1" 30 | } 31 | } -------------------------------------------------------------------------------- /.revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = true 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.range-val-in-closure] 22 | [rule.receiver-naming] 23 | [rule.time-naming] 24 | [rule.unexported-return] 25 | [rule.indent-error-flow] 26 | [rule.errorf] 27 | [rule.empty-block] 28 | [rule.empty-lines] 29 | [rule.superfluous-else] 30 | [rule.unused-parameter] 31 | [rule.unused-receiver] 32 | [rule.unreachable-code] 33 | [rule.redefines-builtin-id] 34 | [rule.atomic] 35 | [rule.deep-exit] 36 | [rule.unnecessary-stmt] 37 | [rule.struct-tag] 38 | [rule.modifies-value-receiver] 39 | [rule.waitgroup-by-value] 40 | [rule.duplicated-imports] 41 | [rule.import-shadowing] 42 | [rule.unhandled-error] 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 William Poussier 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 | -------------------------------------------------------------------------------- /ci/bench.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | rm -f .benchruns 6 | rm -f .benchstats.csv 7 | rm -f .benchstats.json 8 | 9 | echo "Starting benchmark..." 10 | 11 | # Execute benchmarks multiple times. 12 | for i in {1..10} 13 | do 14 | echo " + run #$i" 15 | if [ "$CHARTS" == "true" ]; then 16 | go test -short -bench=. >> .benchruns 17 | else 18 | go test -bench=. >> .benchruns 19 | fi 20 | done 21 | 22 | benchstat .benchruns | tee benchstats 23 | 24 | if [ "$CHARTS" == "true" ]; then 25 | echo -e "\nGenerating charts..." 26 | 27 | # Convert benchmark statistics to CSV and 28 | # transform the output to JSON-formatted 29 | # data tables interpretable by Google Charts. 30 | benchstat -csv -norange .benchruns > .benchstats.csv 31 | go run tools/benchparse/benchparse.go -in .benchstats.csv -out .benchstats.json -omit-bandwidth -omit-allocs 32 | 33 | # Generate chart images and apply trim/border 34 | # operations using ImageMagick. 35 | cd tools/charts && npm --silent --no-audit install && cd ../.. 36 | node tools/charts/index.js -f .benchstats.json -d images/benchmarks -n Simple,Complex,CodeMarshal,Map 37 | fi 38 | 39 | rm -f .benchruns 40 | rm -f .benchstats.csv 41 | rm -f .benchstats.json 42 | -------------------------------------------------------------------------------- /number.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | // isValidNumber returns whether s is a valid JSON 4 | // number literal. Taken from encoding/json. 5 | func isValidNumber(s string) bool { 6 | // This function implements the JSON numbers grammar. 7 | // See https://tools.ietf.org/html/rfc7159#section-6 8 | // and https://www.json.org/img/number.png. 9 | if s == "" { 10 | return false 11 | } 12 | // Optional minus sign. 13 | if s[0] == '-' { 14 | s = s[1:] 15 | if s == "" { 16 | return false 17 | } 18 | } 19 | // Digits. 20 | switch { 21 | default: 22 | return false 23 | case s[0] == '0': 24 | s = s[1:] 25 | case '1' <= s[0] && s[0] <= '9': 26 | s = s[1:] 27 | for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { 28 | s = s[1:] 29 | } 30 | } 31 | // Dot followed by one or more digits. 32 | if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { 33 | s = s[2:] 34 | for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { 35 | s = s[1:] 36 | } 37 | } 38 | // e or E followed by an optional - or + sign 39 | // and one or more digits. 40 | if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { 41 | s = s[1:] 42 | if s[0] == '+' || s[0] == '-' { 43 | s = s[1:] 44 | if s == "" { 45 | return false 46 | } 47 | } 48 | for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { 49 | s = s[1:] 50 | } 51 | } 52 | // Make sure we are at the end. 53 | return s == "" 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE.golang: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /unsafe.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "reflect" 5 | "unsafe" 6 | ) 7 | 8 | // eface is the runtime representation of 9 | // the empty interface. 10 | type eface struct { 11 | rtype unsafe.Pointer 12 | word unsafe.Pointer 13 | } 14 | 15 | // sliceHeader is the runtime representation 16 | // of a slice. 17 | type sliceHeader struct { 18 | Data unsafe.Pointer 19 | Len int 20 | Cap int 21 | } 22 | 23 | // stringHeader is the runtime representation 24 | // of a string. 25 | type stringHeader struct { 26 | Data unsafe.Pointer 27 | Len int 28 | } 29 | 30 | //nolint:staticcheck 31 | //go:nosplit 32 | func noescape(p unsafe.Pointer) unsafe.Pointer { 33 | x := uintptr(p) 34 | return unsafe.Pointer(x ^ 0) 35 | } 36 | 37 | func unpackEface(i interface{}) *eface { 38 | return (*eface)(unsafe.Pointer(&i)) 39 | } 40 | 41 | func packEface(p unsafe.Pointer, t reflect.Type, ptr bool) interface{} { 42 | var i interface{} 43 | e := (*eface)(unsafe.Pointer(&i)) 44 | e.rtype = unpackEface(t).word 45 | 46 | if ptr { 47 | // Value is indirect, but interface is 48 | // direct. We need to load the data at 49 | // p into the interface data word. 50 | e.word = *(*unsafe.Pointer)(p) 51 | } else { 52 | // Value is direct, and so is the interface. 53 | e.word = p 54 | } 55 | return i 56 | } 57 | 58 | // sp2b converts a string pointer to a byte slice. 59 | //go:nosplit 60 | func sp2b(p unsafe.Pointer) []byte { 61 | shdr := (*stringHeader)(p) 62 | return *(*[]byte)(unsafe.Pointer(&sliceHeader{ 63 | Data: shdr.Data, 64 | Len: shdr.Len, 65 | Cap: shdr.Len, 66 | })) 67 | } 68 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | "unsafe" 7 | ) 8 | 9 | var ( 10 | hiterPool sync.Pool // *hiter 11 | mapElemsPool sync.Pool // *mapElems 12 | ) 13 | 14 | // kv represents a map key/value pair. 15 | type kv struct { 16 | key []byte 17 | keyval []byte 18 | } 19 | 20 | type mapElems struct{ s []kv } 21 | 22 | // releaseMapElems zeroes the content of the 23 | // map elements slice and resets the length to 24 | // zero before putting it back to the pool. 25 | func releaseMapElems(me *mapElems) { 26 | for i := range me.s { 27 | me.s[i] = kv{} 28 | } 29 | me.s = me.s[:0] 30 | mapElemsPool.Put(me) 31 | } 32 | 33 | func (m mapElems) Len() int { return len(m.s) } 34 | func (m mapElems) Swap(i, j int) { m.s[i], m.s[j] = m.s[j], m.s[i] } 35 | func (m mapElems) Less(i, j int) bool { return bytes.Compare(m.s[i].key, m.s[j].key) < 0 } 36 | 37 | // hiter is the runtime representation 38 | // of a hashmap iteration structure. 39 | type hiter struct { 40 | key unsafe.Pointer 41 | val unsafe.Pointer 42 | 43 | // remaining fields are ignored but 44 | // present in the struct so that it 45 | // can be zeroed for reuse. 46 | // see hiter in src/runtime/map.go 47 | _ [6]unsafe.Pointer 48 | _ uintptr 49 | _ uint8 50 | _ bool 51 | _ [2]uint8 52 | _ [2]uintptr 53 | } 54 | 55 | var zeroHiter = &hiter{} 56 | 57 | //go:noescape 58 | //go:linkname mapiterinit runtime.mapiterinit 59 | func mapiterinit(unsafe.Pointer, unsafe.Pointer, unsafe.Pointer) 60 | 61 | //go:noescape 62 | //go:linkname mapiternext reflect.mapiternext 63 | func mapiternext(*hiter) 64 | 65 | //go:noescape 66 | //go:linkname maplen reflect.maplen 67 | func maplen(unsafe.Pointer) int 68 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | name: Tests 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | go: 19 | - "1.17.x" 20 | - "1.18.x" 21 | - "1.19.x" 22 | os: 23 | - ubuntu-latest 24 | - macos-latest 25 | - windows-latest 26 | steps: 27 | - name: Install Go 28 | uses: actions/setup-go@v2 29 | with: 30 | go-version: ${{ matrix.go }} 31 | - name: Checkout repository 32 | uses: actions/checkout@v2 33 | - name: Run tests 34 | run: ./ci/test.sh 35 | - name: Upload coverage 36 | uses: codecov/codecov-action@v1 37 | with: 38 | file: coverage.txt 39 | 40 | bench: 41 | name: Benchmarks 42 | needs: test 43 | runs-on: ubuntu-latest 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | go: 48 | - "1.17.x" 49 | - "1.18.x" 50 | - "1.19.x" 51 | steps: 52 | - name: Install Go 53 | uses: actions/setup-go@v2 54 | with: 55 | go-version: ${{ matrix.go }} 56 | - name: Checkout repository 57 | uses: actions/checkout@v2 58 | - name: Install tools 59 | run: go get golang.org/x/perf/cmd/benchstat 60 | env: 61 | GOPROXY: https://proxy.golang.org 62 | - name: Run benchmarks 63 | run: ./ci/bench.sh 64 | - name: Upload statistics 65 | uses: actions/upload-artifact@v2 66 | with: 67 | name: Benchstats-Go${{ matrix.go }} 68 | path: benchstats 69 | -------------------------------------------------------------------------------- /number_test.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | func TestIsValidNumber(t *testing.T) { 9 | // Taken from https://golang.org/src/encoding/json/number_test.go 10 | // Regexp from: https://stackoverflow.com/a/13340826 11 | var re = regexp.MustCompile( 12 | `^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`, 13 | ) 14 | valid := []string{ 15 | "0", 16 | "-0", 17 | "1", 18 | "-1", 19 | "0.1", 20 | "-0.1", 21 | "1234", 22 | "-1234", 23 | "12.34", 24 | "-12.34", 25 | "12E0", 26 | "12E1", 27 | "12e34", 28 | "12E-0", 29 | "12e+1", 30 | "12e-34", 31 | "-12E0", 32 | "-12E1", 33 | "-12e34", 34 | "-12E-0", 35 | "-12e+1", 36 | "-12e-34", 37 | "1.2E0", 38 | "1.2E1", 39 | "1.2e34", 40 | "1.2E-0", 41 | "1.2e+1", 42 | "1.2e-34", 43 | "-1.2E0", 44 | "-1.2E1", 45 | "-1.2e34", 46 | "-1.2E-0", 47 | "-1.2e+1", 48 | "-1.2e-34", 49 | "0E0", 50 | "0E1", 51 | "0e34", 52 | "0E-0", 53 | "0e+1", 54 | "0e-34", 55 | "-0E0", 56 | "-0E1", 57 | "-0e34", 58 | "-0E-0", 59 | "-0e+1", 60 | "-0e-34", 61 | } 62 | for _, tt := range valid { 63 | if !isValidNumber(tt) { 64 | t.Errorf("%s should be valid", tt) 65 | } 66 | if !re.MatchString(tt) { 67 | t.Errorf("%s should be valid but regexp does not match", tt) 68 | } 69 | } 70 | invalid := []string{ 71 | "", 72 | "-", 73 | "invalid", 74 | "1.0.1", 75 | "1..1", 76 | "-1-2", 77 | "012a42", 78 | "01.2", 79 | "012", 80 | "12E12.12", 81 | "1e2e3", 82 | "1e+-2", 83 | "1e--23", 84 | "1e", 85 | "e1", 86 | "1e+", 87 | "1ea", 88 | "1a", 89 | "1.a", 90 | "1.", 91 | "01", 92 | "1.e1", 93 | } 94 | for _, tt := range invalid { 95 | if isValidNumber(tt) { 96 | t.Errorf("%s should be invalid", tt) 97 | } 98 | if re.MatchString(tt) { 99 | t.Errorf("%s should be invalid but matches regexp", tt) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /integer.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "strconv" 5 | "unsafe" 6 | ) 7 | 8 | // nolint:unparam 9 | func encodeInt( 10 | p unsafe.Pointer, dst []byte, _ encOpts, 11 | ) ([]byte, error) { 12 | return strconv.AppendInt(dst, int64(*(*int)(p)), 10), nil 13 | } 14 | 15 | // nolint:unparam 16 | func encodeInt8( 17 | p unsafe.Pointer, dst []byte, _ encOpts, 18 | ) ([]byte, error) { 19 | return strconv.AppendInt(dst, int64(*(*int8)(p)), 10), nil 20 | } 21 | 22 | // nolint:unparam 23 | func encodeInt16( 24 | p unsafe.Pointer, dst []byte, _ encOpts, 25 | ) ([]byte, error) { 26 | return strconv.AppendInt(dst, int64(*(*int16)(p)), 10), nil 27 | } 28 | 29 | // nolint:unparam 30 | func encodeInt32( 31 | p unsafe.Pointer, dst []byte, _ encOpts, 32 | ) ([]byte, error) { 33 | return strconv.AppendInt(dst, int64(*(*int32)(p)), 10), nil 34 | } 35 | 36 | // nolint:unparam 37 | func encodeInt64( 38 | p unsafe.Pointer, dst []byte, _ encOpts, 39 | ) ([]byte, error) { 40 | return strconv.AppendInt(dst, *(*int64)(p), 10), nil 41 | } 42 | 43 | // nolint:unparam 44 | func encodeUint( 45 | p unsafe.Pointer, dst []byte, _ encOpts, 46 | ) ([]byte, error) { 47 | return strconv.AppendUint(dst, uint64(*(*uint)(p)), 10), nil 48 | } 49 | 50 | // nolint:unparam 51 | func encodeUint8( 52 | p unsafe.Pointer, dst []byte, _ encOpts, 53 | ) ([]byte, error) { 54 | return strconv.AppendUint(dst, uint64(*(*uint8)(p)), 10), nil 55 | } 56 | 57 | // nolint:unparam 58 | func encodeUint16( 59 | p unsafe.Pointer, dst []byte, _ encOpts, 60 | ) ([]byte, error) { 61 | return strconv.AppendUint(dst, uint64(*(*uint16)(p)), 10), nil 62 | } 63 | 64 | // nolint:unparam 65 | func encodeUint32( 66 | p unsafe.Pointer, dst []byte, _ encOpts, 67 | ) ([]byte, error) { 68 | return strconv.AppendUint(dst, uint64(*(*uint32)(p)), 10), nil 69 | } 70 | 71 | // nolint:unparam 72 | func encodeUint64( 73 | p unsafe.Pointer, dst []byte, _ encOpts, 74 | ) ([]byte, error) { 75 | return strconv.AppendUint(dst, *(*uint64)(p), 10), nil 76 | } 77 | 78 | // nolint:unparam 79 | func encodeUintptr( 80 | p unsafe.Pointer, dst []byte, _ encOpts, 81 | ) ([]byte, error) { 82 | return strconv.AppendUint(dst, uint64(*(*uintptr)(p)), 10), nil 83 | } 84 | -------------------------------------------------------------------------------- /images/benchmarks/simple.svg: -------------------------------------------------------------------------------- 1 | ns/opB/opjettisonjsonitersegmentjstandard0150300450600 -------------------------------------------------------------------------------- /images/benchmarks/complex.svg: -------------------------------------------------------------------------------- 1 | ns/opB/opjettisonjsonitersegmentjstandard030006000900012000 -------------------------------------------------------------------------------- /images/benchmarks/code-marshal.svg: -------------------------------------------------------------------------------- 1 | ns/opB/opjettisonjsonitersegmentjstandard02000000400000060000008000000 -------------------------------------------------------------------------------- /images/benchmarks/map.svg: -------------------------------------------------------------------------------- 1 | ns/opB/opjettisonjettison-nosortjsonitersegmentjstandard0500100015002000 -------------------------------------------------------------------------------- /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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 5 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 6 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 7 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 8 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 9 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 10 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 11 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= 15 | github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= 16 | github.com/segmentio/encoding v0.3.4 h1:WM4IBnxH8B9TakiM2QD5LyNl9JSndh88QbHqVC+Pauc= 17 | github.com/segmentio/encoding v0.3.4/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 20 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 21 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 22 | golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4= 24 | golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 27 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 28 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 29 | -------------------------------------------------------------------------------- /json_1.14_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.14 2 | 3 | package jettison 4 | 5 | import ( 6 | "encoding" 7 | "encoding/json" 8 | "fmt" 9 | "net" 10 | "strconv" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | type ( 16 | bvtm int 17 | brtm int 18 | bvjm string 19 | brjm string 20 | cvtm struct{ L, R string } 21 | crtm struct{ L, R string } 22 | ) 23 | 24 | func (m bvtm) MarshalText() ([]byte, error) { return []byte(strconv.Itoa(int(m))), nil } 25 | func (m *brtm) MarshalText() ([]byte, error) { return []byte(strconv.Itoa(int(*m))), nil } 26 | func (m bvjm) MarshalJSON() ([]byte, error) { return []byte(strconv.Quote(string(m))), nil } 27 | func (m *brjm) MarshalJSON() ([]byte, error) { return []byte(strconv.Quote(string(*m))), nil } 28 | func (m cvtm) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("%s:%s", m.L, m.R)), nil } 29 | func (m *crtm) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("%s:%s", m.L, m.R)), nil } 30 | 31 | // TestTextMarshalerMapKey tests the marshaling 32 | // of maps with key types that implements the 33 | // encoding.TextMarshaler interface 34 | func TestTextMarshalerMapKey(t *testing.T) { 35 | var ( 36 | bval = bvtm(42) 37 | bref = brtm(84) 38 | cval = cvtm{L: "A", R: "B"} 39 | cref = crtm{L: "A", R: "B"} 40 | ip = &net.IP{127, 0, 0, 1} 41 | ) 42 | valid := []interface{}{ 43 | map[time.Time]string{ 44 | time.Now(): "now", 45 | {}: "", 46 | }, 47 | map[*net.IP]string{ 48 | ip: "localhost", 49 | nil: "", 50 | }, 51 | map[cvtm]string{cval: "ab"}, 52 | map[*cvtm]string{ 53 | &cval: "ab", 54 | nil: "ba", 55 | }, 56 | map[*crtm]string{ 57 | &cref: "ab", 58 | nil: "", 59 | }, 60 | map[bvtm]string{bval: "42"}, 61 | map[*bvtm]string{ 62 | &bval: "42", 63 | nil: "", 64 | }, 65 | map[brtm]string{bref: "42"}, 66 | map[*brtm]string{ 67 | &bref: "42", 68 | nil: "", 69 | }, 70 | } 71 | for _, v := range valid { 72 | marshalCompare(t, v, "valid") 73 | } 74 | invalid := []interface{}{ 75 | // Non-pointer value of a pointer-receiver 76 | // type isn't a valid map key type. 77 | map[crtm]string{ 78 | {L: "A", R: "B"}: "ab", 79 | }, 80 | } 81 | for _, v := range invalid { 82 | marshalCompareError(t, v, "invalid") 83 | } 84 | } 85 | 86 | //nolint:godox 87 | func TestNilMarshaler(t *testing.T) { 88 | testdata := []struct { 89 | v interface{} 90 | }{ 91 | // json.Marshaler 92 | {struct{ M json.Marshaler }{M: nil}}, 93 | {struct{ M json.Marshaler }{(*niljsonm)(nil)}}, 94 | {struct{ M interface{} }{(*niljsonm)(nil)}}, 95 | {struct{ M *niljsonm }{M: nil}}, 96 | {json.Marshaler((*niljsonm)(nil))}, 97 | {(*niljsonm)(nil)}, 98 | 99 | // encoding.TextMarshaler 100 | {struct{ M encoding.TextMarshaler }{M: nil}}, 101 | {struct{ M encoding.TextMarshaler }{(*niltextm)(nil)}}, 102 | {struct{ M interface{} }{(*niltextm)(nil)}}, 103 | {struct{ M *niltextm }{M: nil}}, 104 | {encoding.TextMarshaler((*niltextm)(nil))}, 105 | {(*niltextm)(nil)}, 106 | 107 | // jettison.Marshaler 108 | {struct{ M comboMarshaler }{M: nil}}, 109 | {struct{ M comboMarshaler }{(*niljetim)(nil)}}, 110 | {struct{ M interface{} }{(*niljetim)(nil)}}, 111 | {struct{ M *niljetim }{M: nil}}, 112 | {comboMarshaler((*niljetim)(nil))}, 113 | {(*niljetim)(nil)}, 114 | 115 | // jettison.MarshalerCtx 116 | {struct{ M comboMarshalerCtx }{M: nil}}, 117 | {struct{ M comboMarshalerCtx }{(*nilmjctx)(nil)}}, 118 | {struct{ M interface{} }{(*nilmjctx)(nil)}}, 119 | {struct{ M *nilmjctx }{M: nil}}, 120 | {comboMarshalerCtx((*nilmjctx)(nil))}, 121 | {(*nilmjctx)(nil)}, 122 | } 123 | for _, e := range testdata { 124 | marshalCompare(t, e.v, "nil-marshaler") 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /json_1.13_test.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.14 2 | 3 | package jettison 4 | 5 | import ( 6 | "encoding" 7 | "encoding/json" 8 | "fmt" 9 | "net" 10 | "strconv" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | type ( 16 | bvtm int 17 | brtm int 18 | bvjm string 19 | brjm string 20 | cvtm struct{ L, R string } 21 | crtm struct{ L, R string } 22 | ) 23 | 24 | func (m bvtm) MarshalText() ([]byte, error) { return []byte(strconv.Itoa(int(m))), nil } 25 | func (m *brtm) MarshalText() ([]byte, error) { return []byte(strconv.Itoa(int(*m))), nil } 26 | func (m bvjm) MarshalJSON() ([]byte, error) { return []byte(strconv.Quote(string(m))), nil } 27 | func (m *brjm) MarshalJSON() ([]byte, error) { return []byte(strconv.Quote(string(*m))), nil } 28 | func (m cvtm) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("%s:%s", m.L, m.R)), nil } 29 | func (m *crtm) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("%s:%s", m.L, m.R)), nil } 30 | 31 | // TestTextMarshalerMapKey tests the marshaling 32 | // of maps with key types that implements the 33 | // encoding.TextMarshaler interface 34 | func TestTextMarshalerMapKey(t *testing.T) { 35 | var ( 36 | bval = bvtm(42) 37 | bref = brtm(84) 38 | cval = cvtm{L: "A", R: "B"} 39 | cref = crtm{L: "A", R: "B"} 40 | ip = &net.IP{127, 0, 0, 1} 41 | ) 42 | // The nil key cases, although supported by this library, 43 | // isn't tested with verions of Go prior to 1.14, because 44 | // the standard library panics on it, and thus, the results 45 | // cannot be compared. 46 | valid := []interface{}{ 47 | map[time.Time]string{ 48 | time.Now(): "now", 49 | {}: "", 50 | }, 51 | map[*net.IP]string{ 52 | ip: "localhost", 53 | }, 54 | map[cvtm]string{cval: "ab"}, 55 | map[*cvtm]string{ 56 | &cval: "ab", 57 | }, 58 | map[*crtm]string{ 59 | &cref: "ab", 60 | }, 61 | map[bvtm]string{bval: "42"}, 62 | map[*bvtm]string{ 63 | &bval: "42", 64 | }, 65 | map[brtm]string{bref: "42"}, 66 | map[*brtm]string{ 67 | &bref: "42", 68 | }, 69 | } 70 | for _, v := range valid { 71 | marshalCompare(t, v, "valid") 72 | } 73 | invalid := []interface{}{ 74 | // Non-pointer value of a pointer-receiver 75 | // type isn't a valid map key type. 76 | map[crtm]string{ 77 | {L: "A", R: "B"}: "ab", 78 | }, 79 | } 80 | for _, v := range invalid { 81 | marshalCompareError(t, v, "invalid") 82 | } 83 | } 84 | 85 | //nolint:godox 86 | func TestNilMarshaler(t *testing.T) { 87 | testdata := []struct { 88 | v interface{} 89 | }{ 90 | // json.Marshaler 91 | {struct{ M json.Marshaler }{M: nil}}, 92 | {struct{ M json.Marshaler }{(*niljsonm)(nil)}}, 93 | {struct{ M interface{} }{(*niljsonm)(nil)}}, 94 | {struct{ M *niljsonm }{M: nil}}, 95 | {json.Marshaler((*niljsonm)(nil))}, 96 | {(*niljsonm)(nil)}, 97 | 98 | // encoding.TextMarshaler 99 | {struct{ M encoding.TextMarshaler }{(*niltextm)(nil)}}, 100 | {struct{ M interface{} }{(*niltextm)(nil)}}, 101 | {struct{ M *niltextm }{M: nil}}, 102 | {encoding.TextMarshaler((*niltextm)(nil))}, 103 | {(*niltextm)(nil)}, 104 | 105 | // The following case panics with versions of Go 106 | // prior to 1.14. See this issue for reference: 107 | // https://github.com/golang/go/issues/34235 108 | // {struct{ M encoding.TextMarshaler }{M: nil}}, 109 | 110 | // jettison.Marshaler 111 | {struct{ M comboMarshaler }{M: nil}}, 112 | {struct{ M comboMarshaler }{(*niljetim)(nil)}}, 113 | {struct{ M interface{} }{(*niljetim)(nil)}}, 114 | {struct{ M *niljetim }{M: nil}}, 115 | {comboMarshaler((*niljetim)(nil))}, 116 | {(*niljetim)(nil)}, 117 | 118 | // jettison.MarshalerCtx 119 | {struct{ M comboMarshalerCtx }{M: nil}}, 120 | {struct{ M comboMarshalerCtx }{(*nilmjctx)(nil)}}, 121 | {struct{ M interface{} }{(*nilmjctx)(nil)}}, 122 | {struct{ M *nilmjctx }{M: nil}}, 123 | {comboMarshalerCtx((*nilmjctx)(nil))}, 124 | {(*nilmjctx)(nil)}, 125 | } 126 | for _, e := range testdata { 127 | marshalCompare(t, e.v, "nil-marshaler") 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strconv" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestDurationFmtString(t *testing.T) { 12 | testdata := []struct { 13 | fmt DurationFmt 14 | str string 15 | }{ 16 | {DurationString, "str"}, 17 | {DurationMinutes, "min"}, 18 | {DurationSeconds, "s"}, 19 | {DurationMilliseconds, "ms"}, 20 | {DurationMicroseconds, "μs"}, 21 | {DurationNanoseconds, "nanosecond"}, 22 | {DurationFmt(-1), "unknown"}, 23 | {DurationFmt(6), "unknown"}, 24 | } 25 | for _, tt := range testdata { 26 | if s := tt.fmt.String(); s != tt.str { 27 | t.Errorf("got %q, want %q", s, tt.str) 28 | } 29 | } 30 | } 31 | 32 | func TestIssue2(t *testing.T) { 33 | type x struct { 34 | F time.Duration `json:"foobar" yaml:"foobar"` 35 | } 36 | xx := &x{} 37 | b, err := MarshalOpts(xx, DurationFormat(DurationString)) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | const want = `{"foobar":"0s"}` 42 | 43 | if s := string(b); s != want { 44 | t.Errorf("expected %q, got %q", want, s) 45 | } 46 | } 47 | 48 | func TestAppendDuration(t *testing.T) { 49 | // Taken from https://golang.org/src/time/time_test.go 50 | var testdata = []struct { 51 | str string 52 | dur time.Duration 53 | }{ 54 | {"0s", 0}, 55 | {"1ns", 1 * time.Nanosecond}, 56 | {"1.1µs", 1100 * time.Nanosecond}, 57 | {"2.2ms", 2200 * time.Microsecond}, 58 | {"3.3s", 3300 * time.Millisecond}, 59 | {"4m5s", 4*time.Minute + 5*time.Second}, 60 | {"4m5.001s", 4*time.Minute + 5001*time.Millisecond}, 61 | {"5h6m7.001s", 5*time.Hour + 6*time.Minute + 7001*time.Millisecond}, 62 | {"8m0.000000001s", 8*time.Minute + 1*time.Nanosecond}, 63 | {"2562047h47m16.854775807s", 1<<63 - 1}, 64 | {"-2562047h47m16.854775808s", -1 << 63}, 65 | } 66 | for _, tt := range testdata { 67 | buf := appendDuration(make([]byte, 0, 32), tt.dur) 68 | 69 | if s := string(buf); s != tt.str { 70 | t.Errorf("got %q, want %q", s, tt.str) 71 | } 72 | if tt.dur > 0 { 73 | buf = make([]byte, 0, 32) 74 | buf = appendDuration(buf, -tt.dur) 75 | if s := string(buf); s != "-"+tt.str { 76 | t.Errorf("got %q, want %q", s, "-"+tt.str) 77 | } 78 | } 79 | } 80 | } 81 | 82 | func TestAppendRFC3339Time(t *testing.T) { 83 | rand.Seed(time.Now().UnixNano()) 84 | var ( 85 | bat int 86 | buf []byte 87 | ) 88 | for _, nano := range []bool{true, false} { 89 | for i := 0; i < 1e3; i++ { 90 | if testing.Short() && i > 1e2 { 91 | break 92 | } 93 | // Generate a location with a random offset 94 | // between 0 and 12 hours. 95 | off := rand.Intn(12*60 + 1) 96 | if rand.Intn(2) == 0 { // coin flip 97 | off = -off 98 | } 99 | loc := time.FixedZone("", off) 100 | 101 | // Generate a random time between now and the 102 | // Unix epoch, with random fractional seconds. 103 | ts := rand.Int63n(time.Now().Unix() + 1) 104 | tm := time.Unix(ts, rand.Int63n(999999999+1)).In(loc) 105 | 106 | layout := time.RFC3339 107 | if nano { 108 | layout = time.RFC3339Nano 109 | } 110 | bat = len(buf) 111 | buf = appendRFC3339Time(tm, buf, nano) 112 | 113 | // The time encodes with double-quotes. 114 | want := strconv.Quote(tm.Format(layout)) 115 | 116 | if s := string(buf[bat:]); s != want { 117 | t.Errorf("got %s, want %s", s, want) 118 | } 119 | } 120 | } 121 | } 122 | 123 | //nolint:scopelint 124 | func BenchmarkRFC3339Time(b *testing.B) { 125 | if testing.Short() { 126 | b.SkipNow() 127 | } 128 | tm := time.Now() 129 | 130 | for _, tt := range []struct { 131 | name string 132 | layout string 133 | }{ 134 | {"", time.RFC3339}, 135 | {"-nano", time.RFC3339Nano}, 136 | } { 137 | b.Run(fmt.Sprintf("%s%s", "jettison", tt.name), func(b *testing.B) { 138 | b.ReportAllocs() 139 | for i := 0; i < b.N; i++ { 140 | buf := make([]byte, 32) 141 | appendRFC3339Time(tm, buf, tt.layout == time.RFC3339Nano) 142 | } 143 | }) 144 | b.Run(fmt.Sprintf("%s%s", "standard", tt.name), func(b *testing.B) { 145 | b.ReportAllocs() 146 | for i := 0; i < b.N; i++ { 147 | buf := make([]byte, 32) 148 | tm.AppendFormat(buf, tt.layout) 149 | } 150 | }) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tools/benchparse/benchparse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | omitMem = flag.Bool("omit-mem", false, "omit B/op stat") 16 | omitAllocs = flag.Bool("omit-allocs", false, "omit allocs/op stat") 17 | omitBandwidth = flag.Bool("omit-bandwidth", false, "omit MB/s stat") 18 | flagInput = flag.String("in", "benchstats.csv", "csv-formatted benchstat file") 19 | flagOutput = flag.String("out", "benchstats.json", "json-formatted data table file") 20 | ) 21 | 22 | func usage() { 23 | fmt.Fprintf(os.Stderr, "usage: benchparse [options]\n") 24 | fmt.Fprintf(os.Stderr, "options:\n") 25 | flag.PrintDefaults() 26 | os.Exit(2) 27 | } 28 | 29 | type set map[string][]benchStat 30 | 31 | type benchStat struct { 32 | Name string 33 | Value float64 34 | Unit string 35 | } 36 | 37 | type gcDataTables map[string][][]interface{} 38 | 39 | func main() { 40 | log.SetPrefix("benchparse: ") 41 | log.SetFlags(0) 42 | flag.Usage = usage 43 | flag.Parse() 44 | 45 | file, err := os.Open(*flagInput) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | defer file.Close() 50 | 51 | stats, err := parse(bufio.NewScanner(file)) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | out, err := os.OpenFile(*flagOutput, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | defer out.Close() 60 | 61 | data := transform(stats) 62 | 63 | enc := json.NewEncoder(out) 64 | if err := enc.Encode(data); err != nil { 65 | log.Fatal(err) 66 | } 67 | if err := out.Sync(); err != nil { 68 | log.Fatal(err) 69 | } 70 | os.Exit(0) 71 | } 72 | 73 | func parse(scanr *bufio.Scanner) (set, error) { 74 | var ( 75 | i int 76 | unit string 77 | stats = make(set) 78 | ) 79 | // The first line is always the header of the 80 | // first section. All the next section headers 81 | // will be detected thanks to the empty line 82 | // that precede them. 83 | nextIsHdr := true 84 | 85 | for ; scanr.Scan(); i++ { 86 | line := scanr.Text() 87 | 88 | if len(line) == 0 { 89 | nextIsHdr = true 90 | continue 91 | } 92 | r := strings.Split(line, ",") 93 | 94 | if nextIsHdr { 95 | if len(r) != 2 { 96 | return nil, fmt.Errorf("invalid header at line %d", i) 97 | } 98 | s := strings.Split(r[1], " ")[1] 99 | unit = strings.Trim(s, "()") 100 | nextIsHdr = false 101 | continue 102 | } 103 | idx := strings.Index(r[0], "/") 104 | if idx == -1 { 105 | return nil, fmt.Errorf("invalid run name at line %d: no separator", i) 106 | } 107 | bname, rname := r[0][:idx], r[0][idx+1:] 108 | if idx := strings.LastIndexByte(rname, '-'); idx != -1 { 109 | rname = rname[:idx] 110 | } 111 | if _, ok := stats[bname]; !ok { 112 | stats[bname] = nil 113 | } 114 | if len(r) <= 1 { 115 | continue 116 | } 117 | f, err := strconv.ParseFloat(r[1], 64) 118 | if err != nil { 119 | return nil, fmt.Errorf("invalid float value at line %d: %s", i, err) 120 | } 121 | stats[bname] = append(stats[bname], benchStat{ 122 | Name: rname, 123 | Value: f, 124 | Unit: unit, 125 | }) 126 | } 127 | if err := scanr.Err(); err != nil { 128 | return nil, err 129 | } 130 | return stats, nil 131 | } 132 | 133 | // transform converts a stats set to a 2D data-table 134 | // interpretable by the Google Charts API. 135 | func transform(stats set) gcDataTables { 136 | data := make(gcDataTables) 137 | 138 | for bname, stats := range stats { 139 | values := make(map[string][]interface{}) 140 | L: 141 | for _, s := range stats { 142 | if _, ok := values[s.Name]; !ok { 143 | values[s.Name] = append(values[s.Name], s.Name) 144 | } 145 | switch s.Unit { 146 | case "MB/s": 147 | if *omitBandwidth { 148 | continue L 149 | } 150 | case "B/op": // total memory allocated 151 | if *omitMem { 152 | continue L 153 | } 154 | case "allocs/op": // number of allocs 155 | if *omitAllocs { 156 | continue L 157 | } 158 | } 159 | values[s.Name] = append(values[s.Name], s.Value) 160 | } 161 | data[bname] = append(data[bname], []interface{}{"Name", "ns/op"}) 162 | if !*omitBandwidth { 163 | data[bname][0] = append(data[bname][0], "MB/s") 164 | } 165 | if !*omitMem { 166 | data[bname][0] = append(data[bname][0], "B/op") 167 | } 168 | if !*omitAllocs { 169 | data[bname][0] = append(data[bname][0], "allocs/op") 170 | } 171 | for _, v := range values { 172 | data[bname] = append(data[bname], v) 173 | } 174 | } 175 | return data 176 | } 177 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project are documented in this file. 4 | 5 | **THIS LIBRARY IS STILL IN ALPHA AND THERE ARE NO GUARANTEES REGARDING API STABILITY YET** 6 | 7 | ## [v0.7.4] - 2022-03-21 8 | 9 | :warning: Starting from this version, [Go 1.17+](https://golang.org/doc/install) is required to use this package. 10 | 11 | - Fix `reflect.mapiterinit` function prototype for go1.18, changed in https://github.com/golang/go/commit/1b2d794ca3ba60c2dbc958a271662784a7122739. 12 | - Update module version to `1.18` and fix build constraint lines. 13 | 14 | ## [v0.7.3] - 2021-11-02 15 | - Fix the encoding of zero-value time.Duration type in string format. 16 | 17 | ## [v0.7.2] - 2021-08-31 18 | - Minor performances improvements (remove inlined functions in `appendEscapedBytes`). 19 | 20 | ## [v0.7.1] - 2020-03-03 21 | - Fix a regression in the marshaling of `nil` map keys that implement the `encoding.TextMarshaler` interface, intoduced during refactor in version [**v0.5.0**](https://github.com/wI2L/jettison/compare/v0.4.1...v0.5.0). 22 | - Split `TestTextMarshalerMapKey` and `TestNilMarshaler` tests with build constraints to allow previously ignored cases to run with Go1.14. 23 | 24 | ## [v0.7.0] - 2020-02-17 25 | - Add the `omitnil` field tag's option, which specifies that a field with a nil pointer should be omitted from the encoding. This option has precedence over the `omitempty` option. See this issue for more informations about the original proposal: [#22480](https://golang.org/issue/22480). 26 | 27 | ## [v0.6.0] - 2020-02-14 28 | - Add support for the `sync.Map` type. The marshaling behavior for this type is similar to the one of the Go `map`. 29 | 30 | ## [v0.5.0] - 2020-02-02 31 | #### Refactor of the entire project. 32 | This includes the following changes, but not limited to: 33 | 34 | - Remove the `Encoder` type to simplify the usage of the library and stick more closely to the design of `encoding/json` 35 | - Reduce the number of closures used. This improves readability of stacktraces and performance profiles. 36 | - Improve the marshaling performances of many types. 37 | - Add support for marshaling `json.RawMessage` values. 38 | - Add new options `DenyList`, `NoNumberValidation`, `NoCompact`, and rename some others. 39 | - Replace the `Marshaler` and `MarshalerCtx` interfaces by `AppendMarshaler` and `AppendMarshalerCtx` to follow the new *append* model. See this issue for more details: [#34701](https://golang.org/issue/34701). 40 | - Remove the `IntegerBase` option, which didn't worked properly with the `string` JSON tag. 41 | 42 | > Some of the improvements have been inspired by the **github.com/segmentio/encoding** project. 43 | 44 | ## [v0.4.1] - 2019-10-23 45 | - Fix unsafe misuses reported by go vet and the new `-d=checkptr` cmd/compile flag introduced in the Go1.14 development tree by *Matthew Dempsky*. The issues were mostly related to invalid arithmetic operations and dereferences. 46 | - Fix map key types precedence order during marshaling. Keys of any string type are used directly instead of the `MarshalText` method, if the types also implement the `encoding.TextMarshaler` interface. 47 | 48 | ## [v0.4.0] - 2019-10-18 49 | - Add the `Marshaler` interface. Types that implements it can write a JSON representation of themselves to a `Writer` directly, to avoid having to allocate a buffer as they would usually do when using the `json.Marshaler` interface. 50 | 51 | ## [v0.3.1] - 2019-10-09 52 | - Fix HTML characters escaping in struct field names. 53 | - Add examples for Marshal, MarshalTo and Encoder's Encode. 54 | - Refactor string encoding to be compliant with `encoding/json`. 55 | 56 | ## [v0.3.0] - 2019-09-23 57 | - Add global functions `Marshal`, `MarshalTo` and `Register`. 58 | - Update `README.md`: usage, examples and benchmarks. 59 | 60 | ## [v0.2.1] - 2019-09-10 61 | - Refactor instructions for types implementing the `json.Marshaler` and `encoding.TextMarshaler` interfaces. 62 | - Fix encoding of `nil` instances. 63 | - Fix behavior for pointer and non-pointer receivers, to comply with `encoding/json`. 64 | - Fix bug that prevents tagged fields to dominate untagged fields. 65 | - Add support for anonymous struct pointer fields. 66 | - Improve tests coverage of `encoder.go`. 67 | - Add test cases for unexported non-embedded struct fields. 68 | 69 | ## [v0.2.0] - 2019-09-01 70 | - Add support for `json.Number`. 71 | - Update `README.md` to add a Go1.12+ requirement notice. 72 | 73 | ## [v0.1.0] - 2019-08-30 74 | Initial realease. 75 | 76 | [v0.7.4]: https://github.com/wI2L/jettison/compare/v0.7.3...v0.7.4 77 | [v0.7.3]: https://github.com/wI2L/jettison/compare/v0.7.2...v0.7.3 78 | [v0.7.2]: https://github.com/wI2L/jettison/compare/v0.7.1...v0.7.2 79 | [v0.7.1]: https://github.com/wI2L/jettison/compare/v0.7.0...v0.7.1 80 | [v0.7.0]: https://github.com/wI2L/jettison/compare/v0.6.0...v0.7.0 81 | [v0.6.0]: https://github.com/wI2L/jettison/compare/v0.5.0...v0.6.0 82 | [v0.5.0]: https://github.com/wI2L/jettison/compare/v0.4.1...v0.5.0 83 | [v0.4.1]: https://github.com/wI2L/jettison/compare/v0.4.0...v0.4.1 84 | [v0.4.0]: https://github.com/wI2L/jettison/compare/v0.3.1...v0.4.0 85 | [v0.3.1]: https://github.com/wI2L/jettison/compare/v0.3.0...v0.3.1 86 | [v0.3.0]: https://github.com/wI2L/jettison/compare/v0.2.1...v0.3.0 87 | [v0.2.1]: https://github.com/wI2L/jettison/compare/v0.2.0...v0.2.1 88 | [v0.2.0]: https://github.com/wI2L/jettison/compare/0.1.0...v0.2.0 89 | [v0.1.0]: https://github.com/wI2L/jettison/releases/tag/0.1.0 90 | -------------------------------------------------------------------------------- /tools/charts/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const jd = require('jsdom') 5 | const { 6 | JSDOM 7 | } = jd 8 | const path = require('path') 9 | const prog = require('commander') 10 | const util = require('util') 11 | const deca = require('decamelize') 12 | const { optimize } = require('svgo'); 13 | 14 | prog 15 | .option('-f, --input ', 'json-formatted benchmark data file') 16 | .option('-d, --directory ', 'output directory of charts') 17 | .option('-n, --names ', 'comma-separated list of benchmark names', (value) => { 18 | return value.split(',') 19 | }) 20 | 21 | prog.version('0.2.0') 22 | prog.parse(process.argv) 23 | 24 | // createChart creates and exports a Google Bar Chart 25 | // as a PNG image, using the decamelized form of name 26 | // with '-' as separator. The parameter data must be a 27 | // two-dimensional array where each row represents a 28 | // bar in the chart. 29 | function createBarChart (dom, name, data, opts) { 30 | var head = dom.window.document.getElementsByTagName('head')[0] 31 | var func = head.insertBefore 32 | 33 | // Prevent call to Google Font API. 34 | head.insertBefore = function (el, ref) { 35 | if (el.href && el.href.indexOf('//fonts.googleapis.com/css?family=Input') > -1) { 36 | return 37 | } 38 | func.call(head, el, ref) 39 | } 40 | // Add an event listener to draw the chart once 41 | // the DOM is fully loaded. 42 | dom.window.addEventListener('DOMContentLoaded', function () { 43 | const g = dom.window.google 44 | 45 | // Load the Google Visualization 46 | // API and the corechart package. 47 | g.charts.load('45.2', { 48 | packages: ['corechart', 'bar'] 49 | }) 50 | g.charts.setOnLoadCallback(function () { 51 | drawBarChart(dom, name, data, opts) 52 | }) 53 | }) 54 | } 55 | 56 | // exportChartAsSVG exports the SVG of the chart to 57 | // the current working directory. The file name is a 58 | // decamelized version of name using the hyphen char 59 | // as separator, in lowercase. 60 | function exportChartAsSVG (e, name) { 61 | var filename = util.format('%s.svg', 62 | path.join(prog.directory, deca(name, '-')) 63 | ) 64 | var svgEl = e.getElementsByTagName('svg')[0] 65 | var svg = htmlToElement(svgEl.outerHTML) 66 | 67 | svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg') 68 | svg.setAttribute('version', '1.1') 69 | 70 | const result = optimize(svg.outerHTML, { 71 | plugins: [{ 72 | name: "sortAttrs" 73 | }, { 74 | name: "removeAttrs", 75 | params: { 76 | attrs: "(clip-path|aria-label|overflow)" 77 | } 78 | }], 79 | multipass: true 80 | }) 81 | try { 82 | return fs.writeFileSync(filename, result.data) 83 | } catch (err) { 84 | console.error('cannot write svg chart %s: %s', filename, err) 85 | } 86 | } 87 | 88 | function htmlToElement (html) { 89 | const d = (new JSDOM('...')).window.document 90 | var t = d.createElement('template') 91 | t.innerHTML = html.trim() 92 | return t.content.firstChild 93 | } 94 | 95 | function drawBarChart (dom, name, data, opts) { 96 | const g = dom.window.google 97 | const d = dom.window.document 98 | 99 | // Convert the data 2D-array to a DataTable, 100 | // and sort it in alphabetical order using 101 | // the content of the first column which 102 | // contains the names of the benchmark runs. 103 | var dt = new g.visualization.arrayToDataTable(data) 104 | dt.sort([{ 105 | column: 0 106 | }]) 107 | var e = d.getElementById('chart') 108 | var c = new g.visualization.ColumnChart(e) 109 | 110 | // Setup a callback that exports the chart as 111 | // a PNG image when it has finished drawing. 112 | g.visualization.events.addListener(c, 'ready', function () { 113 | exportChartAsSVG(e, name) 114 | }) 115 | c.draw(dt, opts) 116 | } 117 | 118 | // Load and parse the JSON-formatted benchmark 119 | // statistics from the input file. 120 | var file = fs.readFileSync(prog.input) 121 | var data = JSON.parse(file) 122 | 123 | // Iterate over all benchmarks and create the 124 | // chart only if it was requested through the 125 | // command-line parameters. 126 | Object.keys(data).forEach(function (key) { 127 | if (!prog.names.includes(key)) { 128 | return 129 | } 130 | const pageFile = path.join(__dirname, 'jsdom.html') 131 | 132 | JSDOM.fromFile(pageFile, { 133 | resources: 'usable', 134 | runScripts: 'dangerously', 135 | pretendToBeVisual: true 136 | }).then(dom => { 137 | createBarChart(dom, key, data[key], { 138 | width: 700, 139 | height: 400, 140 | chartArea: { 141 | left: 100, 142 | top: 50, 143 | width: '70%', 144 | height: '75%' 145 | }, 146 | vAxis: { 147 | format: '', 148 | gridlines: { 149 | count: 5 150 | }, 151 | minorGridlines: { 152 | count: 2 153 | } 154 | }, 155 | hAxis: { 156 | textStyle: { 157 | bold: true, 158 | fontName: 'Input' 159 | } 160 | } 161 | }) 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "encoding" 5 | "encoding/json" 6 | "reflect" 7 | "sync" 8 | "time" 9 | "unsafe" 10 | ) 11 | 12 | var ( 13 | timeTimeType = reflect.TypeOf(time.Time{}) 14 | timeDurationType = reflect.TypeOf(time.Duration(0)) 15 | syncMapType = reflect.TypeOf((*sync.Map)(nil)).Elem() 16 | jsonNumberType = reflect.TypeOf(json.Number("")) 17 | jsonRawMessageType = reflect.TypeOf(json.RawMessage(nil)) 18 | jsonMarshalerType = reflect.TypeOf((*json.Marshaler)(nil)).Elem() 19 | textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() 20 | appendMarshalerType = reflect.TypeOf((*AppendMarshaler)(nil)).Elem() 21 | appendMarshalerCtxType = reflect.TypeOf((*AppendMarshalerCtx)(nil)).Elem() 22 | ) 23 | 24 | var emptyFnCache sync.Map // map[reflect.Type]emptyFunc 25 | 26 | // emptyFunc is a function that returns whether a 27 | // value pointed by an unsafe.Pointer represents the 28 | // zero value of its type. 29 | type emptyFunc func(unsafe.Pointer) bool 30 | 31 | // marshalerEncodeFunc is a function that appends 32 | // the result of a marshaler method call to dst. 33 | type marshalerEncodeFunc func(interface{}, []byte, encOpts, reflect.Type) ([]byte, error) 34 | 35 | func isBasicType(t reflect.Type) bool { 36 | return isBoolean(t) || isString(t) || isFloatingPoint(t) || isInteger(t) 37 | } 38 | 39 | func isBoolean(t reflect.Type) bool { return t.Kind() == reflect.Bool } 40 | func isString(t reflect.Type) bool { return t.Kind() == reflect.String } 41 | 42 | func isFloatingPoint(t reflect.Type) bool { 43 | kind := t.Kind() 44 | if kind == reflect.Float32 || kind == reflect.Float64 { 45 | return true 46 | } 47 | return false 48 | } 49 | 50 | func isInteger(t reflect.Type) bool { 51 | switch t.Kind() { 52 | case reflect.Int, 53 | reflect.Int8, 54 | reflect.Int16, 55 | reflect.Int32, 56 | reflect.Int64, 57 | reflect.Uint, 58 | reflect.Uint8, 59 | reflect.Uint16, 60 | reflect.Uint32, 61 | reflect.Uint64, 62 | reflect.Uintptr: 63 | return true 64 | default: 65 | return false 66 | } 67 | } 68 | 69 | func isInlined(t reflect.Type) bool { 70 | switch t.Kind() { 71 | case reflect.Ptr, reflect.Map: 72 | return true 73 | case reflect.Struct: 74 | return t.NumField() == 1 && isInlined(t.Field(0).Type) 75 | default: 76 | return false 77 | } 78 | } 79 | 80 | func isNilable(t reflect.Type) bool { 81 | switch t.Kind() { 82 | case reflect.Ptr, reflect.Interface, reflect.Slice, reflect.Map: 83 | return true 84 | } 85 | return false 86 | } 87 | 88 | // cachedEmptyFuncOf is similar to emptyFuncOf, but 89 | // returns a cached function, to avoid duplicates. 90 | func cachedEmptyFuncOf(t reflect.Type) emptyFunc { 91 | if fn, ok := emptyFnCache.Load(t); ok { 92 | return fn.(emptyFunc) 93 | } 94 | fn, _ := emptyFnCache.LoadOrStore(t, emptyFuncOf(t)) 95 | return fn.(emptyFunc) 96 | } 97 | 98 | // emptyFuncOf returns a function that can be used to 99 | // determine if a value pointed by an unsafe,Pointer 100 | // represents the zero-value of type t. 101 | func emptyFuncOf(t reflect.Type) emptyFunc { 102 | switch t.Kind() { 103 | case reflect.Bool: 104 | return func(p unsafe.Pointer) bool { 105 | return !*(*bool)(p) 106 | } 107 | case reflect.String: 108 | return func(p unsafe.Pointer) bool { 109 | return (*stringHeader)(p).Len == 0 110 | } 111 | case reflect.Int: 112 | return func(p unsafe.Pointer) bool { 113 | return *(*int)(p) == 0 114 | } 115 | case reflect.Int8: 116 | return func(p unsafe.Pointer) bool { 117 | return *(*int8)(p) == 0 118 | } 119 | case reflect.Int16: 120 | return func(p unsafe.Pointer) bool { 121 | return *(*int16)(p) == 0 122 | } 123 | case reflect.Int32: 124 | return func(p unsafe.Pointer) bool { 125 | return *(*int32)(p) == 0 126 | } 127 | case reflect.Int64: 128 | return func(p unsafe.Pointer) bool { 129 | return *(*int64)(p) == 0 130 | } 131 | case reflect.Uint: 132 | return func(p unsafe.Pointer) bool { 133 | return *(*uint)(p) == 0 134 | } 135 | case reflect.Uint8: 136 | return func(p unsafe.Pointer) bool { 137 | return *(*uint8)(p) == 0 138 | } 139 | case reflect.Uint16: 140 | return func(p unsafe.Pointer) bool { 141 | return *(*uint16)(p) == 0 142 | } 143 | case reflect.Uint32: 144 | return func(p unsafe.Pointer) bool { 145 | return *(*uint32)(p) == 0 146 | } 147 | case reflect.Uint64: 148 | return func(p unsafe.Pointer) bool { 149 | return *(*uint64)(p) == 0 150 | } 151 | case reflect.Uintptr: 152 | return func(p unsafe.Pointer) bool { 153 | return *(*uintptr)(p) == 0 154 | } 155 | case reflect.Float32: 156 | return func(p unsafe.Pointer) bool { 157 | return *(*float32)(p) == 0 158 | } 159 | case reflect.Float64: 160 | return func(p unsafe.Pointer) bool { 161 | return *(*float64)(p) == 0 162 | } 163 | case reflect.Map: 164 | return func(p unsafe.Pointer) bool { 165 | return maplen(*(*unsafe.Pointer)(p)) == 0 166 | } 167 | case reflect.Ptr: 168 | return func(p unsafe.Pointer) bool { 169 | return *(*unsafe.Pointer)(p) == nil 170 | } 171 | case reflect.Interface: 172 | return func(p unsafe.Pointer) bool { 173 | return *(*unsafe.Pointer)(p) == nil 174 | } 175 | case reflect.Slice: 176 | return func(p unsafe.Pointer) bool { 177 | return (*sliceHeader)(p).Len == 0 178 | } 179 | case reflect.Array: 180 | if t.Len() == 0 { 181 | return func(unsafe.Pointer) bool { return true } 182 | } 183 | } 184 | return func(unsafe.Pointer) bool { return false } 185 | } 186 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "runtime" 8 | ) 9 | 10 | // AppendMarshaler is a variant of the json.Marshaler 11 | // interface, implemented by types that can append a 12 | // valid and compact JSON representation of themselves 13 | // to a buffer. If a type implements both interfaces, 14 | // this one will be used in priority by the package. 15 | type AppendMarshaler interface { 16 | AppendJSON([]byte) ([]byte, error) 17 | } 18 | 19 | // AppendMarshalerCtx is similar to AppendMarshaler, 20 | // but the method implemented also takes a context. 21 | // The use case for this interface is to dynamically 22 | // control the marshaling of the type implementing it 23 | // through the values encapsulated by the context, 24 | // that may be provided at runtime using WithContext. 25 | type AppendMarshalerCtx interface { 26 | AppendJSONContext(context.Context, []byte) ([]byte, error) 27 | } 28 | 29 | const ( 30 | marshalerJSON = "MarshalJSON" 31 | marshalerText = "MarshalText" 32 | marshalerAppendJSONCtx = "AppendJSONContext" 33 | marshalerAppendJSON = "AppendJSON" 34 | ) 35 | 36 | // MarshalerError represents an error from calling 37 | // the methods MarshalJSON or MarshalText. 38 | type MarshalerError struct { 39 | Type reflect.Type 40 | Err error 41 | funcName string 42 | } 43 | 44 | // Error implements the builtin error interface. 45 | func (e *MarshalerError) Error() string { 46 | return fmt.Sprintf("json: error calling %s for type %s: %s", 47 | e.funcName, e.Type, e.Err.Error()) 48 | } 49 | 50 | // Unwrap returns the error wrapped by e. 51 | // This doesn't implement a public interface, but 52 | // allow to use the errors.Unwrap function released 53 | // in Go1.13 with a MarshalerError. 54 | func (e *MarshalerError) Unwrap() error { 55 | return e.Err 56 | } 57 | 58 | // UnsupportedTypeError is the error returned 59 | // by Marshal when attempting to encode an 60 | // unsupported value type. 61 | type UnsupportedTypeError struct { 62 | Type reflect.Type 63 | } 64 | 65 | // Error implements the bultin error interface. 66 | func (e *UnsupportedTypeError) Error() string { 67 | return fmt.Sprintf("json: unsupported type: %s", e.Type) 68 | } 69 | 70 | // UnsupportedValueError is the error returned 71 | // by Marshal when attempting to encode an 72 | // unsupported value. 73 | type UnsupportedValueError struct { 74 | Value reflect.Value 75 | Str string 76 | } 77 | 78 | // Error implements the builtin error interface. 79 | func (e *UnsupportedValueError) Error() string { 80 | return fmt.Sprintf("json: unsupported value: %s", e.Str) 81 | } 82 | 83 | // A SyntaxError is a description of a JSON syntax error. 84 | // Unlike its equivalent in the encoding/json package, the 85 | // Error method implemented does not return a meaningful 86 | // message, and the Offset field is always zero. 87 | // It is present merely for consistency. 88 | type SyntaxError struct { 89 | msg string 90 | Offset int64 91 | } 92 | 93 | // Error implements the builtin error interface. 94 | func (e *SyntaxError) Error() string { return e.msg } 95 | 96 | // InvalidOptionError is the error returned by 97 | // MarshalOpts when one of the given options is 98 | // invalid. 99 | type InvalidOptionError struct { 100 | Err error 101 | } 102 | 103 | // Error implements the builtin error interface. 104 | func (e *InvalidOptionError) Error() string { 105 | return fmt.Sprintf("json: invalid option: %s", e.Err.Error()) 106 | } 107 | 108 | // Marshal returns the JSON encoding of v. 109 | // The full documentation can be found at 110 | // https://golang.org/pkg/encoding/json/#Marshal. 111 | func Marshal(v interface{}) ([]byte, error) { 112 | if v == nil { 113 | return []byte("null"), nil 114 | } 115 | return marshalJSON(v, defaultEncOpts()) 116 | } 117 | 118 | // Append is similar to Marshal but appends the JSON 119 | // representation of v to dst instead of returning a 120 | // new allocated slice. 121 | func Append(dst []byte, v interface{}) ([]byte, error) { 122 | if v == nil { 123 | return append(dst, "null"...), nil 124 | } 125 | return appendJSON(dst, v, defaultEncOpts()) 126 | } 127 | 128 | // MarshalOpts is similar to Marshal, but also accepts 129 | // a list of options to configure the encoding behavior. 130 | func MarshalOpts(v interface{}, opts ...Option) ([]byte, error) { 131 | if v == nil { 132 | return []byte("null"), nil 133 | } 134 | eo := defaultEncOpts() 135 | 136 | if len(opts) != 0 { 137 | (&eo).apply(opts...) 138 | if err := eo.validate(); err != nil { 139 | return nil, &InvalidOptionError{err} 140 | } 141 | } 142 | return marshalJSON(v, eo) 143 | } 144 | 145 | // AppendOpts is similar to Append, but also accepts 146 | // a list of options to configure the encoding behavior. 147 | func AppendOpts(dst []byte, v interface{}, opts ...Option) ([]byte, error) { 148 | if v == nil { 149 | return append(dst, "null"...), nil 150 | } 151 | eo := defaultEncOpts() 152 | 153 | if len(opts) != 0 { 154 | (&eo).apply(opts...) 155 | if err := eo.validate(); err != nil { 156 | return nil, &InvalidOptionError{err} 157 | } 158 | } 159 | return appendJSON(dst, v, eo) 160 | } 161 | 162 | func marshalJSON(v interface{}, opts encOpts) ([]byte, error) { 163 | ins := cachedInstr(reflect.TypeOf(v)) 164 | buf := cachedBuffer() 165 | 166 | var err error 167 | buf.B, err = ins(unpackEface(v).word, buf.B, opts) 168 | 169 | // Ensure that v is reachable until 170 | // the instruction has returned. 171 | runtime.KeepAlive(v) 172 | 173 | var b []byte 174 | if err == nil { 175 | // Make a copy of the buffer's content 176 | // before its returned to the pool. 177 | b = make([]byte, len(buf.B)) 178 | copy(b, buf.B) 179 | } 180 | bufferPool.Put(buf) 181 | 182 | return b, err 183 | } 184 | 185 | func appendJSON(dst []byte, v interface{}, opts encOpts) ([]byte, error) { 186 | ins := cachedInstr(reflect.TypeOf(v)) 187 | var err error 188 | dst, err = ins(unpackEface(v).word, dst, opts) 189 | runtime.KeepAlive(v) 190 | 191 | return dst, err 192 | } 193 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import "time" 4 | 5 | const epoch = 62135683200 // 1970-01-01T00:00:00 6 | 7 | // DurationFmt represents the format used 8 | // to encode a time.Duration value. 9 | type DurationFmt int 10 | 11 | // DurationFmt constants. 12 | const ( 13 | DurationString DurationFmt = iota 14 | DurationMinutes 15 | DurationSeconds 16 | DurationMilliseconds 17 | DurationMicroseconds 18 | DurationNanoseconds // default 19 | ) 20 | 21 | // String implements the fmt.Stringer 22 | // interface for DurationFmt. 23 | func (f DurationFmt) String() string { 24 | if !f.valid() { 25 | return "unknown" 26 | } 27 | return durationFmtStr[f] 28 | } 29 | 30 | func (f DurationFmt) valid() bool { 31 | return f >= DurationString && f <= DurationNanoseconds 32 | } 33 | 34 | var ( 35 | zeroDuration = []byte("0s") 36 | durationFmtStr = []string{"str", "min", "s", "ms", "μs", "nanosecond"} 37 | dayOffset = [13]uint16{0, 306, 337, 0, 31, 61, 92, 122, 153, 184, 214, 245, 275} 38 | ) 39 | 40 | // appendDuration appends the textual representation 41 | // of d to the tail of dst and returns the extended buffer. 42 | // Adapted from https://golang.org/src/time/time.go. 43 | func appendDuration(dst []byte, d time.Duration) []byte { 44 | var buf [32]byte 45 | 46 | l := len(buf) 47 | u := uint64(d) 48 | n := d < 0 49 | if n { 50 | u = -u 51 | } 52 | if u < uint64(time.Second) { 53 | // Special case: if duration is smaller than 54 | // a second, use smaller units, like 1.2ms 55 | var prec int 56 | l-- 57 | buf[l] = 's' 58 | l-- 59 | switch { 60 | case u == 0: 61 | return append(dst, zeroDuration...) 62 | case u < uint64(time.Microsecond): 63 | prec = 0 64 | buf[l] = 'n' 65 | case u < uint64(time.Millisecond): 66 | prec = 3 67 | // U+00B5 'µ' micro sign is 0xC2 0xB5. 68 | // Need room for two bytes. 69 | l-- 70 | copy(buf[l:], "µ") 71 | default: // Format as milliseconds. 72 | prec = 6 73 | buf[l] = 'm' 74 | } 75 | l, u = fmtFrac(buf[:l], u, prec) 76 | l = fmtInt(buf[:l], u) 77 | } else { 78 | l-- 79 | buf[l] = 's' 80 | 81 | l, u = fmtFrac(buf[:l], u, 9) 82 | 83 | // Format as seconds. 84 | l = fmtInt(buf[:l], u%60) 85 | u /= 60 86 | 87 | // Format as minutes. 88 | if u > 0 { 89 | l-- 90 | buf[l] = 'm' 91 | l = fmtInt(buf[:l], u%60) 92 | u /= 60 93 | 94 | // Format as hours. Stop there, because 95 | // days can be different lengths. 96 | if u > 0 { 97 | l-- 98 | buf[l] = 'h' 99 | l = fmtInt(buf[:l], u) 100 | } 101 | } 102 | } 103 | if n { 104 | l-- 105 | buf[l] = '-' 106 | } 107 | return append(dst, buf[l:]...) 108 | } 109 | 110 | // fmtInt formats v into the tail of buf. 111 | // It returns the index where the output begins. 112 | // Taken from https://golang.org/src/time/time.go. 113 | func fmtInt(buf []byte, v uint64) int { 114 | w := len(buf) 115 | if v == 0 { 116 | w-- 117 | buf[w] = '0' 118 | } else { 119 | for v > 0 { 120 | w-- 121 | buf[w] = byte(v%10) + '0' 122 | v /= 10 123 | } 124 | } 125 | return w 126 | } 127 | 128 | // fmtFrac formats the fraction of v/10**prec (e.g., ".12345") 129 | // into the tail of buf, omitting trailing zeros. It omits the 130 | // decimal point too when the fraction is 0. It returns the 131 | // index where the output bytes begin and the value v/10**prec. 132 | // Taken from https://golang.org/src/time/time.go. 133 | func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) { 134 | // Omit trailing zeros up to and including decimal point. 135 | w := len(buf) 136 | print := false 137 | for i := 0; i < prec; i++ { 138 | digit := v % 10 139 | print = print || digit != 0 140 | if print { 141 | w-- 142 | buf[w] = byte(digit) + '0' 143 | } 144 | v /= 10 145 | } 146 | if print { 147 | w-- 148 | buf[w] = '.' 149 | } 150 | return w, v 151 | } 152 | 153 | func rdnToYmd(rdn uint32) (uint16, uint16, uint16) { 154 | // Rata Die algorithm by Peter Baum. 155 | var ( 156 | Z = rdn + 306 157 | H = 100*Z - 25 158 | A = H / 3652425 159 | B = A - (A >> 2) 160 | y = (100*B + H) / 36525 161 | d = B + Z - (1461 * y >> 2) 162 | m = (535*d + 48950) >> 14 163 | ) 164 | if m > 12 { 165 | y++ 166 | m -= 12 167 | } 168 | return uint16(y), uint16(m), uint16(d) - dayOffset[m] 169 | } 170 | 171 | // appendRFC3339Time appends the RFC3339 textual representation 172 | // of t to the tail of dst and returns the extended buffer. 173 | // Adapted from https://github.com/chansen/c-timestamp. 174 | func appendRFC3339Time(t time.Time, dst []byte, nano bool) []byte { 175 | var buf [37]byte 176 | 177 | // Base layout chars with opening quote. 178 | buf[0], buf[5], buf[8], buf[11], buf[14], buf[17] = '"', '-', '-', 'T', ':', ':' 179 | 180 | // Year. 181 | _, offset := t.Zone() 182 | sec := t.Unix() + int64(offset) + epoch 183 | y, m, d := rdnToYmd(uint32(sec / 86400)) 184 | for i := 4; i >= 1; i-- { 185 | buf[i] = byte(y%10) + '0' 186 | y /= 10 187 | } 188 | buf[7], m = byte(m%10)+'0', m/10 // month 189 | buf[6] = byte(m%10) + '0' 190 | 191 | buf[10], d = byte(d%10)+'0', d/10 // day 192 | buf[9] = byte(d%10) + '0' 193 | 194 | // Hours/minutes/seconds. 195 | s := sec % 86400 196 | buf[19], s = byte(s%10)+'0', s/10 197 | buf[18], s = byte(s%06)+'0', s/6 198 | buf[16], s = byte(s%10)+'0', s/10 199 | buf[15], s = byte(s%06)+'0', s/6 200 | buf[13], s = byte(s%10)+'0', s/10 201 | buf[12], _ = byte(s%10)+'0', 0 202 | 203 | n := 20 204 | 205 | // Fractional second precision. 206 | nsec := t.Nanosecond() 207 | if nano && nsec != 0 { 208 | buf[n] = '.' 209 | u := nsec 210 | for i := 9; i >= 1; i-- { 211 | buf[n+i] = byte(u%10) + '0' 212 | u /= 10 213 | } 214 | // Remove trailing zeros. 215 | var rpad int 216 | for i := 9; i >= 1; i-- { 217 | if buf[n+i] == '0' { 218 | rpad++ 219 | } else { 220 | break 221 | } 222 | } 223 | n += 10 - rpad 224 | } 225 | // Zone. 226 | if offset == 0 { 227 | buf[n] = 'Z' 228 | n++ 229 | } else { 230 | var z int 231 | zone := offset / 60 // convert to minutes 232 | if zone < 0 { 233 | buf[n] = '-' 234 | z = -zone 235 | } else { 236 | buf[n] = '+' 237 | z = zone 238 | } 239 | buf[n+3] = ':' 240 | buf[n+5], z = byte(z%10)+'0', z/10 241 | buf[n+4], z = byte(z%06)+'0', z/6 242 | buf[n+2], z = byte(z%10)+'0', z/10 243 | buf[n+1], _ = byte(z%10)+'0', 0 244 | n += 6 245 | } 246 | // Finally, add the closing quote. 247 | // It's position depends on the presence 248 | // of the fractional seconds and/or the 249 | // timezone offset. 250 | buf[n] = '"' 251 | 252 | return append(dst, buf[:n+1]...) 253 | } 254 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "context" 7 | "encoding/json" 8 | "io/ioutil" 9 | "os" 10 | "strconv" 11 | "sync" 12 | "testing" 13 | "time" 14 | 15 | jsoniter "github.com/json-iterator/go" 16 | segmentj "github.com/segmentio/encoding/json" 17 | ) 18 | 19 | var jsoniterCfg = jsoniter.ConfigCompatibleWithStandardLibrary 20 | 21 | type marshalFunc func(interface{}) ([]byte, error) 22 | 23 | type codeResponse struct { 24 | Tree *codeNode `json:"tree"` 25 | Username string `json:"username"` 26 | } 27 | 28 | type codeNode struct { 29 | Name string `json:"name"` 30 | Kids []*codeNode `json:"kids"` 31 | CLWeight float64 `json:"cl_weight"` 32 | Touches int `json:"touches"` 33 | MinT int64 `json:"min_t"` 34 | MaxT int64 `json:"max_t"` 35 | MeanT int64 `json:"mean_t"` 36 | } 37 | 38 | type simplePayload struct { 39 | St int `json:"st"` 40 | Sid int `json:"sid"` 41 | Tt string `json:"tt"` 42 | Gr int `json:"gr"` 43 | UUID string `json:"uuid"` 44 | IP string `json:"ip"` 45 | Ua string `json:"ua"` 46 | Tz int `json:"tz"` 47 | V bool `json:"v"` 48 | } 49 | 50 | func BenchmarkSimple(b *testing.B) { 51 | sp := &simplePayload{ 52 | St: 1, 53 | Sid: 2, 54 | Tt: "TestString", 55 | Gr: 4, 56 | UUID: "8f9a65eb-4807-4d57-b6e0-bda5d62f1429", 57 | IP: "127.0.0.1", 58 | Ua: "Mozilla", 59 | Tz: 8, 60 | V: true, 61 | } 62 | benchMarshal(b, sp) 63 | } 64 | 65 | func BenchmarkComplex(b *testing.B) { 66 | benchMarshal(b, xx) 67 | } 68 | 69 | func BenchmarkCodeMarshal(b *testing.B) { 70 | // Taken from the encoding/json package. 71 | x := codeInit(b) 72 | benchMarshal(b, x) 73 | } 74 | 75 | func BenchmarkMap(b *testing.B) { 76 | m := map[string]int{ 77 | "Cassianus": 1, 78 | "Ludovicus": 42, 79 | "Flavius": 8990, 80 | "Baldwin": 345, 81 | "Agapios": -43, 82 | "Liberia": 0, 83 | } 84 | benchMarshal(b, m) 85 | benchMarshalOpts(b, "jettison-nosort", m, UnsortedMap()) 86 | } 87 | 88 | func BenchmarkSyncMap(b *testing.B) { 89 | if testing.Short() { 90 | b.SkipNow() 91 | } 92 | var sm sync.Map 93 | 94 | sm.Store("a", "foobar") 95 | sm.Store("b", 42) 96 | sm.Store("c", false) 97 | sm.Store("d", float64(3.14159)) 98 | 99 | benchMarshalOpts(b, "sorted", m) 100 | benchMarshalOpts(b, "unsorted", m, UnsortedMap()) 101 | } 102 | 103 | func BenchmarkDuration(b *testing.B) { 104 | if testing.Short() { 105 | b.SkipNow() 106 | } 107 | d := 1337 * time.Second 108 | benchMarshal(b, d) 109 | } 110 | 111 | func BenchmarkDurationFormat(b *testing.B) { 112 | if testing.Short() { 113 | b.SkipNow() 114 | } 115 | d := 32*time.Hour + 56*time.Minute + 25*time.Second 116 | for _, f := range []DurationFmt{ 117 | DurationString, 118 | DurationMinutes, 119 | DurationSeconds, 120 | DurationMicroseconds, 121 | DurationMilliseconds, 122 | DurationNanoseconds, 123 | } { 124 | benchMarshalOpts(b, f.String(), d, DurationFormat(f)) 125 | } 126 | } 127 | 128 | func BenchmarkTime(b *testing.B) { 129 | if testing.Short() { 130 | b.SkipNow() 131 | } 132 | t := time.Now() 133 | benchMarshal(b, t) 134 | } 135 | 136 | func BenchmarkStringEscaping(b *testing.B) { 137 | if testing.Short() { 138 | b.SkipNow() 139 | } 140 | s := "<ŁØŘ€M ƗƤŞỮM ĐØŁØŘ ŞƗŦ ΔM€Ŧ>" 141 | 142 | benchMarshalOpts(b, "Full", s) 143 | benchMarshalOpts(b, "NoUTF8Coercion", s, NoUTF8Coercion()) 144 | benchMarshalOpts(b, "NoHTMLEscaping", s, NoHTMLEscaping()) 145 | benchMarshalOpts(b, "NoUTF8Coercion/NoHTMLEscaping", s, NoUTF8Coercion(), NoHTMLEscaping()) 146 | benchMarshalOpts(b, "NoStringEscaping", s, NoStringEscaping()) 147 | } 148 | 149 | type ( 150 | jsonbm struct{} 151 | textbm struct{} 152 | jetibm struct{} 153 | jetictxbm struct{} 154 | ) 155 | 156 | var ( 157 | loreumipsum = "Lorem ipsum dolor sit amet" 158 | loreumipsumQ = strconv.Quote(loreumipsum) 159 | ) 160 | 161 | func (jsonbm) MarshalJSON() ([]byte, error) { return []byte(loreumipsumQ), nil } 162 | func (textbm) MarshalText() ([]byte, error) { return []byte(loreumipsum), nil } 163 | func (jetibm) AppendJSON(dst []byte) ([]byte, error) { return append(dst, loreumipsum...), nil } 164 | 165 | func (jetictxbm) AppendJSONContext(_ context.Context, dst []byte) ([]byte, error) { 166 | return append(dst, loreumipsum...), nil 167 | } 168 | 169 | func BenchmarkMarshaler(b *testing.B) { 170 | if testing.Short() { 171 | b.SkipNow() 172 | } 173 | for _, bb := range []struct { 174 | name string 175 | impl interface{} 176 | opts []Option 177 | }{ 178 | {"json", jsonbm{}, nil}, 179 | {"text", textbm{}, nil}, 180 | {"append", jetibm{}, nil}, 181 | {"appendctx", jetictxbm{}, []Option{WithContext(context.Background())}}, 182 | } { 183 | benchMarshalOpts(b, bb.name, bb.impl, bb.opts...) 184 | } 185 | } 186 | 187 | func codeInit(b *testing.B) *codeResponse { 188 | f, err := os.Open("testdata/code.json.gz") 189 | if err != nil { 190 | b.Fatal(err) 191 | } 192 | defer f.Close() 193 | gz, err := gzip.NewReader(f) 194 | if err != nil { 195 | b.Fatal(err) 196 | } 197 | data, err := ioutil.ReadAll(gz) 198 | if err != nil { 199 | b.Fatal(err) 200 | } 201 | codeJSON := data 202 | 203 | var resp codeResponse 204 | if err := json.Unmarshal(codeJSON, &resp); err != nil { 205 | b.Fatalf("unmarshal code.json: %s", err) 206 | } 207 | if data, err = Marshal(&resp); err != nil { 208 | b.Fatalf("marshal code.json: %s", err) 209 | } 210 | if !bytes.Equal(data, codeJSON) { 211 | b.Logf("different lengths: %d - %d", len(data), len(codeJSON)) 212 | 213 | for i := 0; i < len(data) && i < len(codeJSON); i++ { 214 | if data[i] != codeJSON[i] { 215 | b.Logf("re-marshal: changed at byte %d", i) 216 | b.Logf("old: %s", string(codeJSON[i-10:i+10])) 217 | b.Logf("new: %s", string(data[i-10:i+10])) 218 | break 219 | } 220 | } 221 | b.Fatal("re-marshal code.json: different result") 222 | } 223 | return &resp 224 | } 225 | 226 | func benchMarshalOpts(b *testing.B, name string, x interface{}, opts ...Option) { 227 | b.Run(name, func(b *testing.B) { 228 | b.ReportAllocs() 229 | for i := 0; i < b.N; i++ { 230 | bts, err := MarshalOpts(x, opts...) 231 | if err != nil { 232 | b.Fatal(err) 233 | } 234 | b.SetBytes(int64(len(bts))) 235 | } 236 | }) 237 | } 238 | 239 | func benchMarshal(b *testing.B, x interface{}) { 240 | for _, bb := range []struct { 241 | name string 242 | fn marshalFunc 243 | }{ 244 | {"standard", json.Marshal}, 245 | {"jsoniter", jsoniterCfg.Marshal}, 246 | {"segmentj", segmentj.Marshal}, 247 | {"jettison", Marshal}, 248 | } { 249 | bb := bb 250 | b.Run(bb.name, func(b *testing.B) { 251 | b.ReportAllocs() 252 | for i := 0; i < b.N; i++ { 253 | bts, err := bb.fn(x) 254 | if err != nil { 255 | b.Error(err) 256 | } 257 | b.SetBytes(int64(len(bts))) 258 | } 259 | }) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // defaultTimeLayout is the default layout used 10 | // to format time.Time values. This is compliant 11 | // with the ECMA specification and the JavaScript 12 | // Date's toJSON method implementation. 13 | const defaultTimeLayout = time.RFC3339Nano 14 | 15 | // defaultDurationFmt is the default format used 16 | // to encode time.Duration values. 17 | const defaultDurationFmt = DurationNanoseconds 18 | 19 | // An Option overrides the default encoding 20 | // behavior of the MarshalOpts function. 21 | type Option func(*encOpts) 22 | 23 | type bitmask uint64 24 | 25 | func (b *bitmask) set(f bitmask) { *b |= f } 26 | func (b bitmask) has(f bitmask) bool { return b&f != 0 } 27 | 28 | const ( 29 | unixTime bitmask = 1 << iota 30 | unsortedMap 31 | rawByteSlice 32 | byteArrayAsString 33 | nilMapEmpty 34 | nilSliceEmpty 35 | noStringEscaping 36 | noHTMLEscaping 37 | noUTF8Coercion 38 | noCompact 39 | noNumberValidation 40 | ) 41 | 42 | type encOpts struct { 43 | ctx context.Context 44 | timeLayout string 45 | durationFmt DurationFmt 46 | flags bitmask 47 | allowList stringSet 48 | denyList stringSet 49 | } 50 | 51 | func defaultEncOpts() encOpts { 52 | return encOpts{ 53 | ctx: context.TODO(), 54 | timeLayout: defaultTimeLayout, 55 | durationFmt: defaultDurationFmt, 56 | } 57 | } 58 | 59 | func (eo *encOpts) apply(opts ...Option) { 60 | for _, opt := range opts { 61 | if opt != nil { 62 | opt(eo) 63 | } 64 | } 65 | } 66 | 67 | func (eo encOpts) validate() error { 68 | switch { 69 | case eo.ctx == nil: 70 | return fmt.Errorf("nil context") 71 | case eo.timeLayout == "": 72 | return fmt.Errorf("empty time layout") 73 | case !eo.durationFmt.valid(): 74 | return fmt.Errorf("unknown duration format") 75 | default: 76 | return nil 77 | } 78 | } 79 | 80 | // isDeniedField returns whether a struct field 81 | // identified by its name must be skipped during 82 | // the encoding of a struct. 83 | func (eo encOpts) isDeniedField(name string) bool { 84 | // The deny-list has precedence and must 85 | // be checked first if it has entries. 86 | if eo.denyList != nil { 87 | if _, ok := eo.denyList[name]; ok { 88 | return true 89 | } 90 | } 91 | if eo.allowList != nil { 92 | if _, ok := eo.allowList[name]; !ok { 93 | return true 94 | } 95 | } 96 | return false 97 | } 98 | 99 | type stringSet map[string]struct{} 100 | 101 | func fieldListToSet(list []string) stringSet { 102 | m := make(stringSet) 103 | for _, f := range list { 104 | m[f] = struct{}{} 105 | } 106 | return m 107 | } 108 | 109 | // UnixTime configures an encoder to encode 110 | // time.Time values as Unix timestamps. This 111 | // option, when used, has precedence over any 112 | // time layout confiured. 113 | func UnixTime() Option { 114 | return func(o *encOpts) { o.flags.set(unixTime) } 115 | } 116 | 117 | // UnsortedMap configures an encoder to skip 118 | // the sort of map keys. 119 | func UnsortedMap() Option { 120 | return func(o *encOpts) { o.flags.set(unsortedMap) } 121 | } 122 | 123 | // RawByteSlice configures an encoder to 124 | // encode byte slices as raw JSON strings, 125 | // rather than bas64-encoded strings. 126 | func RawByteSlice() Option { 127 | return func(o *encOpts) { o.flags.set(rawByteSlice) } 128 | } 129 | 130 | // ByteArrayAsString configures an encoder 131 | // to encode byte arrays as raw JSON strings. 132 | func ByteArrayAsString() Option { 133 | return func(o *encOpts) { o.flags.set(byteArrayAsString) } 134 | } 135 | 136 | // NilMapEmpty configures an encoder to 137 | // encode nil Go maps as empty JSON objects, 138 | // rather than null. 139 | func NilMapEmpty() Option { 140 | return func(o *encOpts) { o.flags.set(nilMapEmpty) } 141 | } 142 | 143 | // NilSliceEmpty configures an encoder to 144 | // encode nil Go slices as empty JSON arrays, 145 | // rather than null. 146 | func NilSliceEmpty() Option { 147 | return func(o *encOpts) { o.flags.set(nilSliceEmpty) } 148 | } 149 | 150 | // NoStringEscaping configures an encoder to 151 | // disable string escaping. 152 | func NoStringEscaping() Option { 153 | return func(o *encOpts) { o.flags.set(noStringEscaping) } 154 | } 155 | 156 | // NoHTMLEscaping configures an encoder to 157 | // disable the escaping of problematic HTML 158 | // characters in JSON strings. 159 | func NoHTMLEscaping() Option { 160 | return func(o *encOpts) { o.flags.set(noHTMLEscaping) } 161 | } 162 | 163 | // NoUTF8Coercion configures an encoder to 164 | // disable UTF8 coercion that replace invalid 165 | // bytes with the Unicode replacement rune. 166 | func NoUTF8Coercion() Option { 167 | return func(o *encOpts) { o.flags.set(noUTF8Coercion) } 168 | } 169 | 170 | // NoNumberValidation configures an encoder to 171 | // disable the validation of json.Number values. 172 | func NoNumberValidation() Option { 173 | return func(o *encOpts) { o.flags.set(noNumberValidation) } 174 | } 175 | 176 | // NoCompact configures an encoder to disable 177 | // the compaction of the JSON output produced 178 | // by a call to MarshalJSON, or the content of 179 | // a json.RawMessage. 180 | // see https://golang.org/pkg/encoding/json/#Compact 181 | func NoCompact() Option { 182 | return func(o *encOpts) { o.flags.set(noCompact) } 183 | } 184 | 185 | // TimeLayout sets the time layout used to encode 186 | // time.Time values. The layout must be compatible 187 | // with the Golang time package specification. 188 | func TimeLayout(layout string) Option { 189 | return func(o *encOpts) { 190 | o.timeLayout = layout 191 | } 192 | } 193 | 194 | // DurationFormat sets the format used to encode 195 | // time.Duration values. 196 | func DurationFormat(format DurationFmt) Option { 197 | return func(o *encOpts) { 198 | o.durationFmt = format 199 | } 200 | } 201 | 202 | // WithContext sets the context to use during 203 | // encoding. The context will be passed in to 204 | // the AppendJSONContext method of types that 205 | // implement the AppendMarshalerCtx interface. 206 | func WithContext(ctx context.Context) Option { 207 | return func(o *encOpts) { 208 | o.ctx = ctx 209 | } 210 | } 211 | 212 | // AllowList sets the list of fields which are to be 213 | // considered when encoding a struct. 214 | // The fields are identified by the name that is 215 | // used in the final JSON payload. 216 | // See DenyFields documentation for more information 217 | // regarding joint use with this option. 218 | func AllowList(fields []string) Option { 219 | m := fieldListToSet(fields) 220 | return func(o *encOpts) { 221 | o.allowList = m 222 | } 223 | } 224 | 225 | // DenyList is similar to AllowList, but conversely 226 | // sets the list of fields to omit during encoding. 227 | // When used in conjunction with AllowList, denied 228 | // fields have precedence over the allowed fields. 229 | func DenyList(fields []string) Option { 230 | m := fieldListToSet(fields) 231 | return func(o *encOpts) { 232 | o.denyList = m 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /struct.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "reflect" 7 | "sort" 8 | "strings" 9 | "sync" 10 | "unicode" 11 | ) 12 | 13 | const validChars = "!#$%&()*+-./:<=>?@[]^_{|}~ " 14 | 15 | var fieldsCache sync.Map // map[reflect.Type][]field 16 | 17 | type seq struct { 18 | offset uintptr 19 | indir bool 20 | } 21 | 22 | type field struct { 23 | typ reflect.Type 24 | name string 25 | keyNonEsc []byte 26 | keyEscHTML []byte 27 | index []int 28 | tag bool 29 | quoted bool 30 | omitEmpty bool 31 | omitNil bool 32 | omitNullMarshaler bool 33 | instr instruction 34 | empty emptyFunc 35 | 36 | // embedSeq represents the sequence of offsets 37 | // and indirections to follow to reach the field 38 | // through one or more anonymous fields. 39 | embedSeq []seq 40 | } 41 | 42 | type typeCount map[reflect.Type]int 43 | 44 | // byIndex sorts a list of fields by index sequence. 45 | type byIndex []field 46 | 47 | func (x byIndex) Len() int { return len(x) } 48 | func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 49 | 50 | func (x byIndex) Less(i, j int) bool { 51 | for k, xik := range x[i].index { 52 | if k >= len(x[j].index) { 53 | return false 54 | } 55 | if xik != x[j].index[k] { 56 | return xik < x[j].index[k] 57 | } 58 | } 59 | return len(x[i].index) < len(x[j].index) 60 | } 61 | 62 | // cachedFields is similar to structFields, but uses a 63 | // cache to avoid repeated work. 64 | func cachedFields(t reflect.Type) []field { 65 | if f, ok := fieldsCache.Load(t); ok { 66 | return f.([]field) 67 | } 68 | f, _ := fieldsCache.LoadOrStore(t, structFields(t)) 69 | return f.([]field) 70 | } 71 | 72 | // structFields returns a list of fields that should be 73 | // encoded for the given struct type. The algorithm is 74 | // breadth-first search over the set of structs to include, 75 | // the top one and then any reachable anonymous structs. 76 | func structFields(t reflect.Type) []field { 77 | var ( 78 | flds []field 79 | ccnt typeCount 80 | curr = []field{} 81 | next = []field{{typ: t}} 82 | ncnt = make(typeCount) 83 | seen = make(map[reflect.Type]bool) 84 | ) 85 | for len(next) > 0 { 86 | curr, next = next, curr[:0] 87 | ccnt, ncnt = ncnt, make(map[reflect.Type]int) 88 | 89 | for _, f := range curr { 90 | if seen[f.typ] { 91 | continue 92 | } 93 | seen[f.typ] = true 94 | // Scan the type for fields to encode. 95 | flds, next = scanFields(f, flds, next, ccnt, ncnt) 96 | } 97 | } 98 | sortFields(flds) 99 | 100 | flds = filterByVisibility(flds) 101 | 102 | // Sort fields by their index sequence. 103 | sort.Sort(byIndex(flds)) 104 | 105 | return flds 106 | } 107 | 108 | // sortFields sorts the fields by name, breaking ties 109 | // with depth, then whether the field name come from 110 | // the JSON tag, and finally with the index sequence. 111 | func sortFields(fields []field) { 112 | sort.Slice(fields, func(i int, j int) bool { 113 | x := fields 114 | 115 | if x[i].name != x[j].name { 116 | return x[i].name < x[j].name 117 | } 118 | if len(x[i].index) != len(x[j].index) { 119 | return len(x[i].index) < len(x[j].index) 120 | } 121 | if x[i].tag != x[j].tag { 122 | return x[i].tag 123 | } 124 | return byIndex(x).Less(i, j) 125 | }) 126 | } 127 | 128 | // shouldEncodeField returns whether a struct 129 | // field should be encoded. 130 | func shouldEncodeField(sf reflect.StructField) bool { 131 | isUnexported := sf.PkgPath != "" 132 | if sf.Anonymous { 133 | t := sf.Type 134 | if t.Kind() == reflect.Ptr { 135 | t = t.Elem() 136 | } 137 | // Ignore embedded fields of unexported non-struct 138 | // types, but in the contrary, don't ignore embedded 139 | // fields of unexported struct types since they may 140 | // have exported fields. 141 | if isUnexported && t.Kind() != reflect.Struct { 142 | return false 143 | } 144 | } else if isUnexported { 145 | // Ignore unexported non-embedded fields. 146 | return false 147 | } 148 | return true 149 | } 150 | 151 | // isValidFieldName returns whether s is a valid 152 | // name and can be used as a JSON key to encode 153 | // a struct field. 154 | func isValidFieldName(s string) bool { 155 | if len(s) == 0 { 156 | return false 157 | } 158 | for _, c := range s { 159 | switch { 160 | case strings.ContainsRune(validChars, c): 161 | // Backslash and quote chars are reserved, but 162 | // otherwise any punctuation chars are allowed 163 | // in a tag name. 164 | case !unicode.IsLetter(c) && !unicode.IsDigit(c): 165 | return false 166 | } 167 | } 168 | return true 169 | } 170 | 171 | // filterByVisibility deletes all fields that are hidden 172 | // by the Go rules for embedded fields, except that fields 173 | // with JSON tags are promoted. The fields are sorted in 174 | // primary order of name, secondary order of field index 175 | // length. 176 | func filterByVisibility(fields []field) []field { 177 | ret := fields[:0] 178 | 179 | for adv, i := 0, 0; i < len(fields); i += adv { 180 | // One iteration per name. 181 | // Find the sequence of fields with the name 182 | // of this first field. 183 | fi := fields[i] 184 | for adv = 1; i+adv < len(fields); adv++ { 185 | fj := fields[i+adv] 186 | if fj.name != fi.name { 187 | break 188 | } 189 | } 190 | if adv == 1 { 191 | // Only one field with this name. 192 | ret = append(ret, fi) 193 | continue 194 | } 195 | // More than one field with the same name are 196 | // present, delete hidden fields by choosing 197 | // the dominant field that survives. 198 | if dominant, ok := dominantField(fields[i : i+adv]); ok { 199 | ret = append(ret, dominant) 200 | } 201 | } 202 | return ret 203 | } 204 | 205 | func typeByIndex(t reflect.Type, index []int) reflect.Type { 206 | for _, i := range index { 207 | if t.Kind() == reflect.Ptr { 208 | t = t.Elem() 209 | } 210 | t = t.Field(i).Type 211 | } 212 | return t 213 | } 214 | 215 | // dominantField looks through the fields, all of which 216 | // are known to have the same name, to find the single 217 | // field that dominates the others using Go's embedding 218 | // rules, modified by the presence of JSON tags. If there 219 | // are multiple top-level fields, it returns false. This 220 | // condition is an error in Go, and all fields are skipped. 221 | func dominantField(fields []field) (field, bool) { 222 | if len(fields) > 1 && 223 | len(fields[0].index) == len(fields[1].index) && 224 | fields[0].tag == fields[1].tag { 225 | return field{}, false 226 | } 227 | return fields[0], true 228 | } 229 | 230 | func scanFields(f field, fields, next []field, cnt, ncnt typeCount) ([]field, []field) { 231 | var escBuf bytes.Buffer 232 | 233 | for i := 0; i < f.typ.NumField(); i++ { 234 | sf := f.typ.Field(i) 235 | 236 | if !shouldEncodeField(sf) { 237 | continue 238 | } 239 | tag := sf.Tag.Get("json") 240 | if tag == "-" { 241 | continue 242 | } 243 | // Parse name and options from the content 244 | // of the JSON tag. 245 | name, opts := parseTag(tag) 246 | if !isValidFieldName(name) { 247 | name = "" 248 | } 249 | index := make([]int, len(f.index)+1) 250 | copy(index, f.index) 251 | index[len(f.index)] = i 252 | 253 | typ := sf.Type 254 | isPtr := typ.Kind() == reflect.Ptr 255 | if typ.Name() == "" && isPtr { 256 | typ = typ.Elem() 257 | } 258 | // If the field is a named embedded struct or a 259 | // simple field, record it and its index sequence. 260 | if name != "" || !sf.Anonymous || typ.Kind() != reflect.Struct { 261 | tagged := name != "" 262 | // If a name is not present in the tag, 263 | // use the struct field's name instead. 264 | if name == "" { 265 | name = sf.Name 266 | } 267 | // Build HTML escaped field key. 268 | escBuf.Reset() 269 | _, _ = escBuf.WriteString(`"`) 270 | json.HTMLEscape(&escBuf, []byte(name)) 271 | _, _ = escBuf.WriteString(`":`) 272 | 273 | nf := field{ 274 | typ: typ, 275 | name: name, 276 | tag: tagged, 277 | index: index, 278 | omitEmpty: opts.Contains("omitempty"), 279 | omitNil: opts.Contains("omitnil"), 280 | quoted: opts.Contains("string") && isBasicType(typ), 281 | keyNonEsc: []byte(`"` + name + `":`), 282 | keyEscHTML: append([]byte(nil), escBuf.Bytes()...), // copy 283 | embedSeq: append(f.embedSeq[:0:0], f.embedSeq...), // clone 284 | } 285 | // Add final offset to sequences. 286 | nf.embedSeq = append(nf.embedSeq, seq{sf.Offset, false}) 287 | fields = append(fields, nf) 288 | 289 | if cnt[f.typ] > 1 { 290 | // If there were multiple instances, add a 291 | // second, so that the annihilation code will 292 | // see a duplicate. It only cares about the 293 | // distinction between 1 or 2, so don't bother 294 | // generating any more copies. 295 | fields = append(fields, fields[len(fields)-1]) 296 | } 297 | continue 298 | } 299 | // Record unnamed embedded struct 300 | // to be scanned in the next round. 301 | ncnt[typ]++ 302 | if ncnt[typ] == 1 { 303 | next = append(next, field{ 304 | typ: typ, 305 | name: typ.Name(), 306 | index: index, 307 | embedSeq: append(f.embedSeq, seq{sf.Offset, isPtr}), 308 | }) 309 | } 310 | } 311 | return fields, next 312 | } 313 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package jettison_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/wI2L/jettison" 13 | ) 14 | 15 | func ExampleMarshal() { 16 | type X struct { 17 | A string `json:"a"` 18 | B int64 `json:"b"` 19 | C []string `json:"colors"` 20 | } 21 | x := X{ 22 | A: "Loreum", 23 | B: -42, 24 | C: []string{"blue", "white", "red"}, 25 | } 26 | b, err := jettison.Marshal(x) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | os.Stdout.Write(b) 31 | // Output: 32 | // {"a":"Loreum","b":-42,"colors":["blue","white","red"]} 33 | } 34 | 35 | func ExampleAppend() { 36 | type X struct { 37 | A bool `json:"a"` 38 | B uint32 `json:"b"` 39 | C map[string]string `json:"users"` 40 | } 41 | x := X{ 42 | A: true, 43 | B: 42, 44 | C: map[string]string{ 45 | "bob": "admin", 46 | "jerry": "user", 47 | }, 48 | } 49 | buf, err := jettison.Append([]byte(nil), x) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | os.Stdout.Write(buf) 54 | // Output: 55 | // {"a":true,"b":42,"users":{"bob":"admin","jerry":"user"}} 56 | } 57 | 58 | func ExampleAppendOpts() { 59 | for _, v := range []interface{}{ 60 | nil, 2 * time.Second, 61 | } { 62 | buf, err := jettison.AppendOpts([]byte(nil), v, 63 | jettison.DurationFormat(jettison.DurationString), 64 | ) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | fmt.Printf("%s\n", string(buf)) 69 | } 70 | // Output: 71 | // null 72 | // "2s" 73 | } 74 | 75 | type Animal int 76 | 77 | const ( 78 | Unknown Animal = iota 79 | Gopher 80 | Zebra 81 | ) 82 | 83 | // AppendJSON implements the jettison.AppendMarshaler interface. 84 | func (a Animal) AppendJSON(dst []byte) ([]byte, error) { 85 | var s string 86 | switch a { 87 | default: 88 | s = "unknown" 89 | case Gopher: 90 | s = "gopher" 91 | case Zebra: 92 | s = "zebra" 93 | } 94 | dst = append(dst, strconv.Quote(s)...) 95 | return dst, nil 96 | } 97 | 98 | func Example_customMarshaler() { 99 | zoo := []Animal{ 100 | Unknown, 101 | Zebra, 102 | Gopher, 103 | } 104 | b, err := jettison.Marshal(zoo) 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | os.Stdout.Write(b) 109 | // Output: 110 | // ["unknown","zebra","gopher"] 111 | } 112 | 113 | func ExampleRawByteSlice() { 114 | bs := []byte("Loreum Ipsum") 115 | 116 | for _, opt := range []jettison.Option{ 117 | nil, jettison.RawByteSlice(), 118 | } { 119 | b, err := jettison.MarshalOpts(bs, opt) 120 | if err != nil { 121 | log.Fatal(err) 122 | } 123 | fmt.Printf("%s\n", string(b)) 124 | } 125 | // Output: 126 | // "TG9yZXVtIElwc3Vt" 127 | // "Loreum Ipsum" 128 | } 129 | 130 | func ExampleByteArrayAsString() { 131 | b1 := [6]byte{'L', 'o', 'r', 'e', 'u', 'm'} 132 | b2 := [6]*byte{&b1[0], &b1[1], &b1[2], &b1[3], &b1[4], &b1[5]} 133 | 134 | for _, opt := range []jettison.Option{ 135 | nil, jettison.ByteArrayAsString(), 136 | } { 137 | for _, v := range []interface{}{b1, b2} { 138 | b, err := jettison.MarshalOpts(v, opt) 139 | if err != nil { 140 | log.Fatal(err) 141 | } 142 | fmt.Printf("%s\n", string(b)) 143 | } 144 | } 145 | // Output: 146 | // [76,111,114,101,117,109] 147 | // [76,111,114,101,117,109] 148 | // "Loreum" 149 | // [76,111,114,101,117,109] 150 | } 151 | 152 | func ExampleNilMapEmpty() { 153 | type X struct { 154 | M1 map[string]int 155 | M2 map[int]string 156 | } 157 | x := X{ 158 | M1: map[string]int{}, 159 | M2: nil, 160 | } 161 | for _, opt := range []jettison.Option{ 162 | nil, jettison.NilMapEmpty(), 163 | } { 164 | b, err := jettison.MarshalOpts(x, opt) 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | fmt.Printf("%s\n", string(b)) 169 | } 170 | // Output: 171 | // {"M1":{},"M2":null} 172 | // {"M1":{},"M2":{}} 173 | } 174 | 175 | func ExampleNilSliceEmpty() { 176 | type X struct { 177 | S1 []int 178 | S2 []string 179 | } 180 | x := X{ 181 | S1: []int{}, 182 | S2: nil, 183 | } 184 | for _, opt := range []jettison.Option{ 185 | nil, jettison.NilSliceEmpty(), 186 | } { 187 | b, err := jettison.MarshalOpts(x, opt) 188 | if err != nil { 189 | log.Fatal(err) 190 | } 191 | fmt.Printf("%s\n", string(b)) 192 | } 193 | // Output: 194 | // {"S1":[],"S2":null} 195 | // {"S1":[],"S2":[]} 196 | } 197 | 198 | func ExampleUnixTime() { 199 | t := time.Date(2024, time.December, 24, 12, 24, 42, 0, time.UTC) 200 | 201 | b, err := jettison.MarshalOpts(t, jettison.UnixTime()) 202 | if err != nil { 203 | log.Fatal(err) 204 | } 205 | os.Stdout.Write(b) 206 | // Output: 207 | // 1735043082 208 | } 209 | 210 | func ExampleTimeLayout() { 211 | t := time.Date(2042, time.July, 25, 16, 42, 24, 67850, time.UTC) 212 | 213 | locs := []*time.Location{ 214 | time.UTC, time.FixedZone("WTF", 666), time.FixedZone("LOL", -4242), 215 | } 216 | for _, layout := range []string{ 217 | time.RFC3339, 218 | time.RFC822, 219 | time.RFC1123Z, 220 | time.RFC3339Nano, // default 221 | } { 222 | for _, loc := range locs { 223 | b, err := jettison.MarshalOpts(t.In(loc), jettison.TimeLayout(layout)) 224 | if err != nil { 225 | log.Fatal(err) 226 | } 227 | fmt.Printf("%s\n", string(b)) 228 | } 229 | } 230 | // Output: 231 | // "2042-07-25T16:42:24Z" 232 | // "2042-07-25T16:53:30+00:11" 233 | // "2042-07-25T15:31:42-01:10" 234 | // "25 Jul 42 16:42 UTC" 235 | // "25 Jul 42 16:53 WTF" 236 | // "25 Jul 42 15:31 LOL" 237 | // "Fri, 25 Jul 2042 16:42:24 +0000" 238 | // "Fri, 25 Jul 2042 16:53:30 +0011" 239 | // "Fri, 25 Jul 2042 15:31:42 -0110" 240 | // "2042-07-25T16:42:24.00006785Z" 241 | // "2042-07-25T16:53:30.00006785+00:11" 242 | // "2042-07-25T15:31:42.00006785-01:10" 243 | } 244 | 245 | func ExampleDurationFormat() { 246 | d := 1*time.Hour + 3*time.Minute + 2*time.Second + 66*time.Millisecond 247 | 248 | for _, format := range []jettison.DurationFmt{ 249 | jettison.DurationString, 250 | jettison.DurationMinutes, 251 | jettison.DurationSeconds, 252 | jettison.DurationMilliseconds, 253 | jettison.DurationMicroseconds, 254 | jettison.DurationNanoseconds, 255 | } { 256 | b, err := jettison.MarshalOpts(d, jettison.DurationFormat(format)) 257 | if err != nil { 258 | log.Fatal(err) 259 | } 260 | fmt.Printf("%s\n", string(b)) 261 | } 262 | // Output: 263 | // "1h3m2.066s" 264 | // 63.03443333333333 265 | // 3782.066 266 | // 3782066 267 | // 3782066000 268 | // 3782066000000 269 | } 270 | 271 | func ExampleUnsortedMap() { 272 | m := map[int]string{ 273 | 3: "three", 274 | 1: "one", 275 | 2: "two", 276 | } 277 | b, err := jettison.MarshalOpts(m, jettison.UnsortedMap()) 278 | if err != nil { 279 | log.Fatal(err) 280 | } 281 | var sorted map[int]string 282 | if err := json.Unmarshal(b, &sorted); err != nil { 283 | log.Fatal(err) 284 | } 285 | b, err = jettison.Marshal(sorted) 286 | if err != nil { 287 | log.Fatal(err) 288 | } 289 | os.Stdout.Write(b) 290 | // Output: 291 | // {"1":"one","2":"two","3":"three"} 292 | } 293 | 294 | func ExampleNoCompact() { 295 | rm := json.RawMessage(`{ "a":"b" }`) 296 | for _, opt := range []jettison.Option{ 297 | nil, jettison.NoCompact(), 298 | } { 299 | b, err := jettison.MarshalOpts(rm, opt) 300 | if err != nil { 301 | log.Fatal(err) 302 | } 303 | fmt.Printf("%s\n", string(b)) 304 | } 305 | // Output: 306 | // {"a":"b"} 307 | // { "a":"b" } 308 | } 309 | 310 | func ExampleAllowList() { 311 | type Z struct { 312 | Omega int `json:"ω"` 313 | } 314 | type Y struct { 315 | Pi string `json:"π"` 316 | } 317 | type X struct { 318 | Z Z `json:"Z"` 319 | Alpha string `json:"α"` 320 | Beta string `json:"β"` 321 | Gamma string 322 | Y 323 | } 324 | x := X{ 325 | Z: Z{Omega: 42}, 326 | Alpha: "1", 327 | Beta: "2", 328 | Gamma: "3", 329 | Y: Y{Pi: "4"}, 330 | } 331 | for _, opt := range []jettison.Option{ 332 | nil, jettison.AllowList([]string{"Z", "β", "Gamma", "π"}), 333 | } { 334 | b, err := jettison.MarshalOpts(x, opt) 335 | if err != nil { 336 | log.Fatal(err) 337 | } 338 | fmt.Printf("%s\n", string(b)) 339 | } 340 | // Output: 341 | // {"Z":{"ω":42},"α":"1","β":"2","Gamma":"3","π":"4"} 342 | // {"Z":{},"β":"2","Gamma":"3","π":"4"} 343 | } 344 | 345 | func ExampleDenyList() { 346 | type X struct { 347 | A int `json:"aaAh"` 348 | B bool `json:"buzz"` 349 | C string 350 | D uint 351 | } 352 | x := X{ 353 | A: -42, 354 | B: true, 355 | C: "Loreum", 356 | D: 42, 357 | } 358 | for _, opt := range []jettison.Option{ 359 | nil, jettison.DenyList([]string{"buzz", "D"}), 360 | } { 361 | b, err := jettison.MarshalOpts(x, opt) 362 | if err != nil { 363 | log.Fatal(err) 364 | } 365 | fmt.Printf("%s\n", string(b)) 366 | } 367 | // Output: 368 | // {"aaAh":-42,"buzz":true,"C":"Loreum","D":42} 369 | // {"aaAh":-42,"C":"Loreum"} 370 | } 371 | 372 | type ( 373 | secret string 374 | ctxKey string 375 | ) 376 | 377 | const obfuscateKey = ctxKey("_obfuscate_") 378 | 379 | // AppendJSONContext implements the jettison.AppendMarshalerCtx interface. 380 | func (s secret) AppendJSONContext(ctx context.Context, dst []byte) ([]byte, error) { 381 | out := string(s) 382 | if v := ctx.Value(obfuscateKey); v != nil { 383 | if hide, ok := v.(bool); ok && hide { 384 | out = "**__SECRET__**" 385 | } 386 | } 387 | dst = append(dst, strconv.Quote(out)...) 388 | return dst, nil 389 | } 390 | 391 | func ExampleWithContext() { 392 | sec := secret("v3ryS3nSitiv3P4ssWord") 393 | 394 | b, err := jettison.Marshal(sec) 395 | if err != nil { 396 | log.Fatal(err) 397 | } 398 | fmt.Printf("%s\n", string(b)) 399 | 400 | ctx := context.WithValue(context.Background(), 401 | obfuscateKey, true, 402 | ) 403 | b, err = jettison.MarshalOpts(sec, jettison.WithContext(ctx)) 404 | if err != nil { 405 | log.Fatal(err) 406 | } 407 | fmt.Printf("%s\n", string(b)) 408 | // Output: 409 | // "v3ryS3nSitiv3P4ssWord" 410 | // "**__SECRET__**" 411 | } 412 | -------------------------------------------------------------------------------- /instruction.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | "sync/atomic" 8 | "unsafe" 9 | ) 10 | 11 | var ( 12 | instrCachePtr unsafe.Pointer // *instrCache 13 | structInstrCache sync.Map // map[string]instruction 14 | ) 15 | 16 | // An instruction appends the JSON representation 17 | // of a value pointed by the unsafe.Pointer p to 18 | // dst and returns the extended buffer. 19 | type instruction func(unsafe.Pointer, []byte, encOpts) ([]byte, error) 20 | 21 | // instrCache is an eventually consistent cache that 22 | // maps Go type definitions to dynamically generated 23 | // instructions. The key is unsafe.Pointer instead of 24 | // reflect.Type to improve lookup performance. 25 | type instrCache map[unsafe.Pointer]instruction 26 | 27 | func typeID(t reflect.Type) unsafe.Pointer { 28 | return unpackEface(t).word 29 | } 30 | 31 | // cachedInstr returns an instruction to encode the 32 | // given type from a cache, or create one on the fly. 33 | func cachedInstr(t reflect.Type) instruction { 34 | id := typeID(t) 35 | 36 | if instr, ok := loadInstr(id); ok { 37 | return instr 38 | } 39 | canAddr := t.Kind() == reflect.Ptr 40 | 41 | // canAddr indicates if the input value is addressable. 42 | // At this point, we only need to know if the value is 43 | // a pointer, the others instructions will handle that 44 | // themselves for their type, or pass-by the value. 45 | instr := newInstruction(t, canAddr, false) 46 | if isInlined(t) { 47 | instr = wrapInlineInstr(instr) 48 | } 49 | storeInstr(id, instr, loadCache()) 50 | 51 | return instr 52 | } 53 | 54 | func loadCache() instrCache { 55 | p := atomic.LoadPointer(&instrCachePtr) 56 | return *(*instrCache)(unsafe.Pointer(&p)) 57 | } 58 | 59 | func loadInstr(id unsafe.Pointer) (instruction, bool) { 60 | cache := loadCache() 61 | instr, ok := cache[id] 62 | return instr, ok 63 | } 64 | 65 | func storeInstr(key unsafe.Pointer, instr instruction, cache instrCache) { 66 | newCache := make(instrCache, len(cache)+1) 67 | 68 | // Clone the current cache and add the 69 | // new instruction. 70 | for k, v := range cache { 71 | newCache[k] = v 72 | } 73 | newCache[key] = instr 74 | 75 | atomic.StorePointer( 76 | &instrCachePtr, 77 | *(*unsafe.Pointer)(unsafe.Pointer(&newCache)), 78 | ) 79 | } 80 | 81 | // newInstruction returns an instruction to encode t. 82 | // canAddr and quoted respectively indicates if the 83 | // value to encode is addressable and must be enclosed 84 | // with double-quote character in the output. 85 | func newInstruction(t reflect.Type, canAddr, quoted bool) instruction { 86 | // Go types must be checked first, because a Duration 87 | // is an int64, json.Number is a string, and both would 88 | // be interpreted as a basic type. Also, the time.Time 89 | // type implements the TextMarshaler interface, but we 90 | // want to use a special instruction instead. 91 | if ins := newGoTypeInstr(t); ins != nil { 92 | return ins 93 | } 94 | if ins := newMarshalerTypeInstr(t, canAddr); ins != nil { 95 | return ins 96 | } 97 | if ins := newBasicTypeInstr(t, quoted); ins != nil { 98 | return ins 99 | } 100 | switch t.Kind() { 101 | case reflect.Interface: 102 | return encodeInterface 103 | case reflect.Struct: 104 | return newStructInstr(t, canAddr) 105 | case reflect.Map: 106 | return newMapInstr(t) 107 | case reflect.Slice: 108 | return newSliceInstr(t) 109 | case reflect.Array: 110 | return newArrayInstr(t, canAddr) 111 | case reflect.Ptr: 112 | return newPtrInstr(t, quoted) 113 | } 114 | return newUnsupportedTypeInstr(t) 115 | } 116 | 117 | func newGoTypeInstr(t reflect.Type) instruction { 118 | switch t { 119 | case syncMapType: 120 | return encodeSyncMap 121 | case timeTimeType: 122 | return encodeTime 123 | case timeDurationType: 124 | return encodeDuration 125 | case jsonNumberType: 126 | return encodeNumber 127 | case jsonRawMessageType: 128 | return encodeRawMessage 129 | default: 130 | return nil 131 | } 132 | } 133 | 134 | // newMarshalerTypeInstr returns an instruction to handle 135 | // a type that implement one of the Marshaler, MarshalerCtx, 136 | // json.Marshal, encoding.TextMarshaler interfaces. 137 | func newMarshalerTypeInstr(t reflect.Type, canAddr bool) instruction { 138 | isPtr := t.Kind() == reflect.Ptr 139 | ptrTo := reflect.PtrTo(t) 140 | 141 | switch { 142 | case t.Implements(appendMarshalerCtxType): 143 | return newAppendMarshalerCtxInstr(t, false) 144 | case !isPtr && canAddr && ptrTo.Implements(appendMarshalerCtxType): 145 | return newAppendMarshalerCtxInstr(t, true) 146 | case t.Implements(appendMarshalerType): 147 | return newAppendMarshalerInstr(t, false) 148 | case !isPtr && canAddr && ptrTo.Implements(appendMarshalerType): 149 | return newAppendMarshalerInstr(t, true) 150 | case t.Implements(jsonMarshalerType): 151 | return newJSONMarshalerInstr(t, false) 152 | case !isPtr && canAddr && ptrTo.Implements(jsonMarshalerType): 153 | return newJSONMarshalerInstr(t, true) 154 | case t.Implements(textMarshalerType): 155 | return newTextMarshalerInstr(t, false) 156 | case !isPtr && canAddr && ptrTo.Implements(textMarshalerType): 157 | return newTextMarshalerInstr(t, true) 158 | default: 159 | return nil 160 | } 161 | } 162 | 163 | func newBasicTypeInstr(t reflect.Type, quoted bool) instruction { 164 | var ins instruction 165 | 166 | switch t.Kind() { 167 | case reflect.Bool: 168 | ins = encodeBool 169 | case reflect.String: 170 | return newStringInstr(quoted) 171 | case reflect.Int: 172 | ins = encodeInt 173 | case reflect.Int8: 174 | ins = encodeInt8 175 | case reflect.Int16: 176 | ins = encodeInt16 177 | case reflect.Int32: 178 | ins = encodeInt32 179 | case reflect.Int64: 180 | ins = encodeInt64 181 | case reflect.Uint: 182 | ins = encodeUint 183 | case reflect.Uint8: 184 | ins = encodeUint8 185 | case reflect.Uint16: 186 | ins = encodeUint16 187 | case reflect.Uint32: 188 | ins = encodeUint32 189 | case reflect.Uint64: 190 | ins = encodeUint64 191 | case reflect.Uintptr: 192 | ins = encodeUintptr 193 | case reflect.Float32: 194 | ins = encodeFloat32 195 | case reflect.Float64: 196 | ins = encodeFloat64 197 | default: 198 | return nil 199 | } 200 | if quoted { 201 | return wrapQuotedInstr(ins) 202 | } 203 | return ins 204 | } 205 | 206 | func newStringInstr(quoted bool) instruction { 207 | if quoted { 208 | return encodeQuotedString 209 | } 210 | return encodeString 211 | } 212 | 213 | func newUnsupportedTypeInstr(t reflect.Type) instruction { 214 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 215 | return dst, &UnsupportedTypeError{t} 216 | } 217 | } 218 | 219 | func newPtrInstr(t reflect.Type, quoted bool) instruction { 220 | e := t.Elem() 221 | i := newInstruction(e, true, quoted) 222 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 223 | return encodePointer(p, dst, opts, i) 224 | } 225 | } 226 | 227 | func newAppendMarshalerCtxInstr(t reflect.Type, hasPtr bool) instruction { 228 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 229 | return encodeMarshaler(p, dst, opts, t, hasPtr, encodeAppendMarshalerCtx) 230 | } 231 | } 232 | 233 | func newAppendMarshalerInstr(t reflect.Type, hasPtr bool) instruction { 234 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 235 | return encodeMarshaler(p, dst, opts, t, hasPtr, encodeAppendMarshaler) 236 | } 237 | } 238 | 239 | func newJSONMarshalerInstr(t reflect.Type, hasPtr bool) instruction { 240 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 241 | return encodeMarshaler(p, dst, opts, t, hasPtr, encodeJSONMarshaler) 242 | } 243 | } 244 | 245 | func newTextMarshalerInstr(t reflect.Type, hasPtr bool) instruction { 246 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 247 | return encodeMarshaler(p, dst, opts, t, hasPtr, encodeTextMarshaler) 248 | } 249 | } 250 | 251 | func newStructInstr(t reflect.Type, canAddr bool) instruction { 252 | id := fmt.Sprintf("%p-%t", typeID(t), canAddr) 253 | 254 | if instr, ok := structInstrCache.Load(id); ok { 255 | return instr.(instruction) 256 | } 257 | // To deal with recursive types, populate the 258 | // instructions cache with an indirect func 259 | // before we build it. This type waits on the 260 | // real instruction (ins) to be ready and then 261 | // calls it. This indirect function is only 262 | // used for recursive types. 263 | var ( 264 | wg sync.WaitGroup 265 | ins instruction 266 | ) 267 | wg.Add(1) 268 | i, loaded := structInstrCache.LoadOrStore(id, 269 | instruction(func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 270 | wg.Wait() // few ns/op overhead 271 | return ins(p, dst, opts) 272 | }), 273 | ) 274 | if loaded { 275 | return i.(instruction) 276 | } 277 | // Generate the real instruction and replace 278 | // the indirect func with it. 279 | ins = newStructFieldsInstr(t, canAddr) 280 | wg.Done() 281 | structInstrCache.Store(id, ins) 282 | 283 | return ins 284 | } 285 | 286 | func newStructFieldsInstr(t reflect.Type, canAddr bool) instruction { 287 | if t.NumField() == 0 { 288 | // Fast path for empty struct. 289 | return func(_ unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 290 | return append(dst, "{}"...), nil 291 | } 292 | } 293 | var ( 294 | flds = cachedFields(t) 295 | dupl = append(flds[:0:0], flds...) // clone 296 | ) 297 | for i := range dupl { 298 | f := &dupl[i] 299 | ftyp := typeByIndex(t, f.index) 300 | etyp := ftyp 301 | 302 | if etyp.Kind() == reflect.Ptr { 303 | etyp = etyp.Elem() 304 | } 305 | if f.omitNil && (ftyp.Implements(jsonMarshalerType) || reflect.PtrTo(ftyp).Implements(jsonMarshalerType)) { 306 | f.omitNullMarshaler = true 307 | } 308 | if !isNilable(ftyp) { 309 | // Disable the omitnil option, to 310 | // eliminate a check at runtime. 311 | f.omitNil = false 312 | } 313 | // Generate instruction and empty func of the field. 314 | // Only strings, floats, integers, and booleans 315 | // types can be quoted. 316 | f.instr = newInstruction(ftyp, canAddr, f.quoted && isBasicType(etyp)) 317 | if f.omitEmpty { 318 | f.empty = cachedEmptyFuncOf(ftyp) 319 | } 320 | } 321 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 322 | return encodeStruct(p, dst, opts, dupl) 323 | } 324 | } 325 | 326 | func newArrayInstr(t reflect.Type, canAddr bool) instruction { 327 | var ( 328 | etyp = t.Elem() 329 | size = etyp.Size() 330 | isba = false 331 | ) 332 | // Array elements are addressable if the 333 | // array itself is addressable. 334 | ins := newInstruction(etyp, canAddr, false) 335 | 336 | // Byte arrays does not encode as a string 337 | // by default, this behavior is defined by 338 | // the encoder's options during marshaling. 339 | if etyp.Kind() == reflect.Uint8 { 340 | pe := reflect.PtrTo(etyp) 341 | if !pe.Implements(jsonMarshalerType) && !pe.Implements(textMarshalerType) { 342 | isba = true 343 | } 344 | } 345 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 346 | return encodeArray(p, dst, opts, ins, size, t.Len(), isba) 347 | } 348 | } 349 | 350 | func newSliceInstr(t reflect.Type) instruction { 351 | etyp := t.Elem() 352 | 353 | if etyp.Kind() == reflect.Uint8 { 354 | pe := reflect.PtrTo(etyp) 355 | if !pe.Implements(jsonMarshalerType) && !pe.Implements(textMarshalerType) { 356 | return encodeByteSlice 357 | } 358 | } 359 | // Slice elements are always addressable. 360 | // see https://golang.org/pkg/reflect/#Value.CanAddr 361 | // for reference. 362 | var ( 363 | ins = newInstruction(etyp, true, false) 364 | size = etyp.Size() 365 | ) 366 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 367 | return encodeSlice(p, dst, opts, ins, size) 368 | } 369 | } 370 | 371 | func newMapInstr(t reflect.Type) instruction { 372 | var ( 373 | ki instruction 374 | vi instruction 375 | ) 376 | kt := t.Key() 377 | et := t.Elem() 378 | 379 | if !isString(kt) && !isInteger(kt) && !kt.Implements(textMarshalerType) { 380 | return newUnsupportedTypeInstr(t) 381 | } 382 | // The standard library has a strict precedence order 383 | // for map key types, defined by the documentation of 384 | // the json.Marshal function. That's why we bypass the 385 | // newTypeInstr function if key type is string. 386 | if isString(kt) { 387 | ki = encodeString 388 | } else { 389 | ki = newInstruction(kt, false, false) 390 | } 391 | // Wrap the key instruction for types that 392 | // do not encode with quotes by default. 393 | if !isString(kt) && !kt.Implements(textMarshalerType) { 394 | ki = wrapQuotedInstr(ki) 395 | } 396 | // See issue golang.org/issue/33675 for reference. 397 | if kt.Implements(textMarshalerType) && kt.Kind() == reflect.Ptr { 398 | ki = wrapTextMarshalerNilCheck(ki) 399 | } 400 | vi = newInstruction(et, false, false) 401 | 402 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 403 | return encodeMap(p, dst, opts, t, ki, vi) 404 | } 405 | } 406 | 407 | func wrapInlineInstr(ins instruction) instruction { 408 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 409 | return ins(noescape(unsafe.Pointer(&p)), dst, opts) 410 | } 411 | } 412 | 413 | func wrapQuotedInstr(ins instruction) instruction { 414 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 415 | dst = append(dst, '"') 416 | var err error 417 | dst, err = ins(p, dst, opts) 418 | if err == nil { 419 | dst = append(dst, '"') 420 | } 421 | return dst, err 422 | } 423 | } 424 | 425 | func wrapTextMarshalerNilCheck(ins instruction) instruction { 426 | return func(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 427 | if *(*unsafe.Pointer)(p) == nil { 428 | return append(dst, `""`...), nil 429 | } 430 | return ins(p, dst, opts) 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Jettison

2 |

GoCaptain

Jettison is a fast and flexible JSON encoder for the Go programming language, inspired by bet365/jingo, with a richer features set, aiming at 100% compatibility with the standard library.

3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

14 | 15 | --- 16 | 17 | ## Installation 18 | 19 | Jettison uses [Go modules](https://github.com/golang/go/wiki/Modules). Releases are tagged according to the _SemVer_ format, prefixed with a `v`, starting from *0.2.0*. You can get the latest release using the following command. 20 | 21 | ```console 22 | $ go get github.com/wI2L/jettison@latest 23 | ``` 24 | 25 | :warning: From version `v0.7.4`, the package requires [Go 1.17+](https://golang.org/doc/install) to build, due to the usage of the [new build constraints](https://go.googlesource.com/proposal/+/master/design/draft-gobuild.md). 26 | 27 | ## Key features 28 | 29 | - Fast, see [benchmarks](#benchmarks) 30 | - No dynamic memory allocations in hot paths 31 | - Behavior identical to the standard library by default 32 | - No code generation required 33 | - Clear and concise API 34 | - Configurable with opt-in functional options 35 | - Native support for many standard library types, see [improvements](#improvements) 36 | - Custom `AppendMarshaler` interface to avoid allocations 37 | - Extensive testsuite that compares its output against `encoding/json` 38 | 39 | ## Overview 40 | 41 | The goal of Jettision is to take up the idea introduced by the **bet365/jingo** package and build a fully-featured JSON encoder around it, that comply with the behavior of the [encoding/json](https://golang.org/pkg/encoding/json/) package. Unlike the latter, Jettison does not use reflection during marshaling, but only once to create the instruction set for a given type ahead of time. The drawback to this approach requires to instantiate an instruction-set once for each type that needs to be marshaled, but that is overcomed with a package cache. 42 | 43 | The package aims to have a behavior similar to that of the standard library for all types encoding and struct tags, meaning that the documentation of the `json.Marshal` [function](https://golang.org/pkg/encoding/json/#Marshal) is applicable for Jettison, with a few exceptions described in this [section](#differences-with-encodingjson). As such, most of the tests compare their output against it to guarantee that. 44 | 45 | ### Implementation details 46 | 47 | The main concept of Jettison consists of using pre-build instructions-set to reduce the cost of using the `reflect` package at runtime. When marshaling a value, a set of _instructions_ is recursively generated for its type, which defines how to iteratively encode it. An _instruction_ is a function or a closure, that have all the information required to read the data from memory using _unsafe_ operations (pointer type conversion, arithmetic...) during the instruction set execution. 48 | 49 | ### Differences with `encoding/json` 50 | 51 | All notable differences with the standard library behavior are listed below. Please note that these might evolve with future versions of the package. 52 | 53 | #### Improvements 54 | 55 | - The `time.Time` and `time.Duration` types are handled natively. For time values, the encoder doesn't invoke `MarshalJSON` or `MarshalText`, but use the `time.AppendFormat` [function](https://golang.org/pkg/time/#Time.AppendFormat) instead, and write the result to the stream. Similarly, for durations, it isn't necessary to implements the `json.Marshaler` or `encoding.TextMarshaler` interfaces on a custom wrapper type, the encoder uses the result of one of the methods `Minutes`, `Seconds`, `Nanoseconds` or `String`, based on the duration [format](https://godoc.org/github.com/wI2L/jettison#DurationFmt) configured. 56 | 57 | - The `sync.Map` type is handled natively. The marshaling behavior is similar to the one of a standard Go `map`. The option `UnsortedMap` can also be used in cunjunction with this type to disable the default keys sort. 58 | 59 | - The `omitnil` field tag's option can be used to specify that a field with a nil pointer should be omitted from the encoding. This option has precedence over the `omitempty` option. Note that struct fields that implement the `json.Marshaler` interface will be omitted too, if they return the literal JSON `null` value. 60 | 61 | #### Bugs 62 | 63 | ##### Go1.13 and backward 64 | 65 | - Nil map keys values implementing the `encoding.TextMarshaler` interface are encoded as empty strings, while the `encoding/json` package currently panic because of that. See this [issue](https://github.com/golang/go/issues/33675) for more details.[1] 66 | 67 | - Nil struct fields implementing the `encoding.TextMarshaler` interface are encoded as `null`, while the `encoding/json` package currently panic because of that. See this [issue](https://github.com/golang/go/issues/34235) for more details.[1] 68 | 69 | 1: The issues mentioned above have had their associated CL merged, and was released with Go 1.14. 70 | 71 | ## Usage 72 | 73 | ### Basic 74 | 75 | As stated above, the library behave similarly to the `encoding/json` package. You can simply replace the `json.Marshal` function with `jettison.Marshal`, and expect the same output with better performances. 76 | 77 | ```go 78 | type X struct { 79 | A string `json:"a"` 80 | B int64 `json:"b"` 81 | } 82 | b, err := jettison.Marshal(X{ 83 | A: "Loreum", 84 | B: 42, 85 | }) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | os.Stdout.Write(b) 90 | ``` 91 | ###### Result 92 | ```json 93 | {"a":"Loreum","b":42} 94 | ``` 95 | 96 | ### Advanced 97 | 98 | If more control over the encoding behavior is required, use the `MarshalOpts` function instead. The second parameter is variadic and accept a list of functional opt-in [options](https://godoc.org/github.com/wI2L/jettison#Option) described below: 99 | 100 | | name | description | 101 | |:------------------------:| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 102 | | **`TimeLayout`** | Defines the layout used to encode `time.Time` values. The layout must be compatible with the [AppendFormat](https://golang.org/pkg/time/#Time.AppendFormat) method. | 103 | | **`DurationFormat`** | Defines the format used to encode `time.Duration` values. See the documentation of the `DurationFmt` type for the complete list of formats available. | 104 | | **`UnixTime`** | Encode `time.Time` values as JSON numbers representing Unix timestamps, the number of seconds elapsed since *January 1, 1970 UTC*. This option has precedence over `TimeLayout`. | 105 | | **`UnsortedMap`** | Disables map keys sort. | 106 | | **`ByteArrayAsString`** | Encodes byte arrays as JSON strings rather than JSON arrays. The output is subject to the same escaping rules used for JSON strings, unless the option `NoStringEscaping` is used. | 107 | | **`RawByteSlice`** | Disables the *base64* default encoding used for byte slices. | 108 | | **`NilMapEmpty`** | Encodes nil Go maps as empty JSON objects rather than `null`. | 109 | | **`NilSliceEmpty`** | Encodes nil Go slices as empty JSON arrays rather than `null`. | 110 | | **`NoStringEscaping`** | Disables string escaping. `NoHTMLEscaping` and `NoUTF8Coercion` are ignored when this option is used. | 111 | | **`NoHTMLEscaping`** | Disables the escaping of special HTML characters such as `&`, `<` and `>` in JSON strings. This is similar to `json.Encoder.SetEscapeHTML(false)`. | 112 | | **`NoUTF8Coercion`** | Disables the replacement of invalid bytes with the Unicode replacement rune in JSON strings. | 113 | | **`AllowList`** | Sets a whitelist that represents which fields are to be encoded when marshaling a Go struct. | 114 | | **`DenyList`** | Sets a blacklist that represents which fields are ignored during the marshaling of a Go struct. | 115 | | **`NoCompact`** | Disables the compaction of JSON output produced by `MarshalJSON` method, and `json.RawMessage` values. | 116 | | **`NoNumberValidation`** | Disables the validation of `json.Number` values. | 117 | | **`WithContext`** | Sets the `context.Context` to be passed to invocations of `AppendJSONContext` methods. | 118 | 119 | Take a look at the [examples](example_test.go) to see these options in action. 120 | 121 | ## Benchmarks 122 | 123 | If you'd like to run the benchmarks yourself, use the following command. 124 | 125 | ```shell 126 | go get github.com/cespare/prettybench 127 | go test -bench=. | prettybench 128 | ``` 129 | 130 | ### Results `-short` 131 | 132 | These benchmarks were run 10x (statistics computed with [benchstat](https://godoc.org/golang.org/x/perf/cmd/benchstat)) on a MacBook Pro 15", with the following specs: 133 | ``` 134 | OS: macOS Catalina (10.15.7) 135 | CPU: 2.6 GHz Intel Core i7 136 | Mem: 16GB 137 | Go: go version go1.17 darwin/amd64 138 | Tag: v0.7.2 139 | ``` 140 | 141 |
Stats
142 | name                    time/op
143 | Simple/standard-8          573ns ± 1%
144 | Simple/jsoniter-8          547ns ± 0%
145 | Simple/segmentj-8          262ns ± 1%
146 | Simple/jettison-8          408ns ± 1%
147 | Complex/standard-8        11.7µs ± 0%
148 | Complex/jsoniter-8        11.6µs ± 1%
149 | Complex/segmentj-8        7.96µs ± 0%
150 | Complex/jettison-8        5.90µs ± 1%
151 | CodeMarshal/standard-8    6.71ms ± 0%
152 | CodeMarshal/jsoniter-8    6.35ms ± 1%
153 | CodeMarshal/segmentj-8    4.38ms ± 1%
154 | CodeMarshal/jettison-8    5.56ms ± 1%
155 | Map/standard-8            1.83µs ± 1%
156 | Map/jsoniter-8            1.65µs ± 0%
157 | Map/segmentj-8            1.61µs ± 0%
158 | Map/jettison-8             772ns ± 1%
159 | Map/jettison-nosort-8      507ns ± 1%
160 | 
161 | name                    speed
162 | Simple/standard-8        236MB/s ± 1%
163 | Simple/jsoniter-8        247MB/s ± 0%
164 | Simple/segmentj-8        516MB/s ± 1%
165 | Simple/jettison-8        331MB/s ± 1%
166 | Complex/standard-8      72.9MB/s ± 0%
167 | Complex/jsoniter-8      70.6MB/s ± 0%
168 | Complex/segmentj-8       108MB/s ± 0%
169 | Complex/jettison-8       144MB/s ± 1%
170 | CodeMarshal/standard-8   289MB/s ± 0%
171 | CodeMarshal/jsoniter-8   306MB/s ± 1%
172 | CodeMarshal/segmentj-8   443MB/s ± 1%
173 | CodeMarshal/jettison-8   349MB/s ± 1%
174 | Map/standard-8          46.6MB/s ± 1%
175 | Map/jsoniter-8          51.5MB/s ± 0%
176 | Map/segmentj-8          52.8MB/s ± 0%
177 | Map/jettison-8           110MB/s ± 1%
178 | Map/jettison-nosort-8    168MB/s ± 1%
179 | 
180 | name                    alloc/op
181 | Simple/standard-8           144B ± 0%
182 | Simple/jsoniter-8           152B ± 0%
183 | Simple/segmentj-8           144B ± 0%
184 | Simple/jettison-8           144B ± 0%
185 | Complex/standard-8        4.05kB ± 0%
186 | Complex/jsoniter-8        3.95kB ± 0%
187 | Complex/segmentj-8        2.56kB ± 0%
188 | Complex/jettison-8          935B ± 0%
189 | CodeMarshal/standard-8    1.97MB ± 0%
190 | CodeMarshal/jsoniter-8    2.00MB ± 0%
191 | CodeMarshal/segmentj-8    1.98MB ± 2%
192 | CodeMarshal/jettison-8    1.98MB ± 2%
193 | Map/standard-8              888B ± 0%
194 | Map/jsoniter-8              884B ± 0%
195 | Map/segmentj-8              576B ± 0%
196 | Map/jettison-8             96.0B ± 0%
197 | Map/jettison-nosort-8       160B ± 0%
198 | 
199 | name                    allocs/op
200 | Simple/standard-8           1.00 ± 0%
201 | Simple/jsoniter-8           2.00 ± 0%
202 | Simple/segmentj-8           1.00 ± 0%
203 | Simple/jettison-8           1.00 ± 0%
204 | Complex/standard-8          79.0 ± 0%
205 | Complex/jsoniter-8          71.0 ± 0%
206 | Complex/segmentj-8          52.0 ± 0%
207 | Complex/jettison-8          8.00 ± 0%
208 | CodeMarshal/standard-8      1.00 ± 0%
209 | CodeMarshal/jsoniter-8      2.00 ± 0%
210 | CodeMarshal/segmentj-8      1.00 ± 0%
211 | CodeMarshal/jettison-8      1.00 ± 0%
212 | Map/standard-8              19.0 ± 0%
213 | Map/jsoniter-8              14.0 ± 0%
214 | Map/segmentj-8              18.0 ± 0%
215 | Map/jettison-8              1.00 ± 0%
216 | Map/jettison-nosort-8       2.00 ± 0%
217 | 
218 | 219 | #### Simple [[source](https://github.com/wI2L/jettison/blob/master/bench_test.go#L50)] 220 | 221 | Basic payload with fields of type `string`, `int` and `bool`. 222 | 223 | ![Simple Benchmark Graph](./images/benchmarks/simple.svg) 224 | 225 | #### Complex [[source](https://github.com/wI2L/jettison/blob/master/bench_test.go#L65)] 226 | 227 | Large payload with a variety of composite Go types, such as `struct`, `map`, `interface`, multi-dimensions `array` and `slice`, with pointer and non-pointer value types. 228 | 229 | Please note that this test is somewhat positively influenced by the performances of map marshaling. 230 | 231 | ![Complex Benchmark Graph](./images/benchmarks/complex.svg) 232 | 233 | #### CodeMarshal [[source](https://github.com/wI2L/jettison/blob/master/bench_test.go#L69)] 234 | 235 | Borrowed from the `encoding/json` tests. See [testdata/code.json.gz](testdata/code.json.gz). 236 | 237 | ![CodeMarshal Benchmark Graph](./images/benchmarks/code-marshal.svg) 238 | 239 | #### Map [[source](https://github.com/wI2L/jettison/blob/master/bench_test.go#L75)] 240 | 241 | Simple `map[string]int` with 6 keys. 242 | 243 | ![Map Graph](./images/benchmarks/map.svg) 244 | 245 | ## Credits 246 | 247 | This library and its design has been inspired by the work of others at **@bet365** and **@segmentio**. 248 | See the following projects for reference: 249 | - [bet365/jingo](https://github.com/bet365/jingo) 250 | - [segmentio/encoding](https://github.com/segmentio/encoding) 251 | 252 | ## License 253 | 254 | Jettison is licensed under the **MIT** license. See the [LICENSE](LICENSE) file. 255 | 256 | This package also uses some portions of code from the Go **encoding/json** package. The associated license can be found in [LICENSE.golang](LICENSE.golang). 257 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "bytes" 5 | "encoding" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "math" 11 | "reflect" 12 | "runtime" 13 | "sort" 14 | "strconv" 15 | "sync" 16 | "time" 17 | "unicode/utf8" 18 | "unsafe" 19 | ) 20 | 21 | const hex = "0123456789abcdef" 22 | 23 | //nolint:unparam 24 | func encodeBool(p unsafe.Pointer, dst []byte, _ encOpts) ([]byte, error) { 25 | if *(*bool)(p) { 26 | return append(dst, "true"...), nil 27 | } 28 | return append(dst, "false"...), nil 29 | } 30 | 31 | // encodeString appends the escaped bytes of the string 32 | // pointed by p to dst. If quoted is true, escaped double 33 | // quote characters are added at the beginning and the 34 | // end of the JSON string. 35 | // nolint:unparam 36 | func encodeString(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 37 | dst = append(dst, '"') 38 | dst = appendEscapedBytes(dst, sp2b(p), opts) 39 | dst = append(dst, '"') 40 | 41 | return dst, nil 42 | } 43 | 44 | //nolint:unparam 45 | func encodeQuotedString(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 46 | dst = append(dst, `"\"`...) 47 | dst = appendEscapedBytes(dst, sp2b(p), opts) 48 | dst = append(dst, `\""`...) 49 | 50 | return dst, nil 51 | } 52 | 53 | // encodeFloat32 appends the textual representation of 54 | // the 32-bits floating point number pointed by p to dst. 55 | func encodeFloat32(p unsafe.Pointer, dst []byte, _ encOpts) ([]byte, error) { 56 | return appendFloat(dst, float64(*(*float32)(p)), 32) 57 | } 58 | 59 | // encodeFloat64 appends the textual representation of 60 | // the 64-bits floating point number pointed by p to dst. 61 | func encodeFloat64(p unsafe.Pointer, dst []byte, _ encOpts) ([]byte, error) { 62 | return appendFloat(dst, *(*float64)(p), 64) 63 | } 64 | 65 | func encodeInterface(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 66 | v := *(*interface{})(p) 67 | if v == nil { 68 | return append(dst, "null"...), nil 69 | } 70 | typ := reflect.TypeOf(v) 71 | ins := cachedInstr(typ) 72 | 73 | return ins(unpackEface(v).word, dst, opts) 74 | } 75 | 76 | func encodeNumber(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 77 | // Cast pointer to string directly to avoid 78 | // a useless conversion. 79 | num := *(*string)(p) 80 | 81 | // In Go1.5 the empty string encodes to "0". 82 | // While this is not a valid number literal, 83 | // we keep compatibility, so check validity 84 | // after this. 85 | if num == "" { 86 | num = "0" // Number's zero-val 87 | } 88 | if !opts.flags.has(noNumberValidation) && !isValidNumber(num) { 89 | return dst, fmt.Errorf("json: invalid number literal %q", num) 90 | } 91 | return append(dst, num...), nil 92 | } 93 | 94 | func encodeRawMessage(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 95 | v := *(*json.RawMessage)(p) 96 | if v == nil { 97 | return append(dst, "null"...), nil 98 | } 99 | if opts.flags.has(noCompact) { 100 | return append(dst, v...), nil 101 | } 102 | return appendCompactJSON(dst, v, !opts.flags.has(noHTMLEscaping)) 103 | } 104 | 105 | // encodeTime appends the time.Time value pointed by 106 | // p to dst based on the format configured in opts. 107 | func encodeTime(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 108 | t := *(*time.Time)(p) 109 | y := t.Year() 110 | 111 | if y < 0 || y >= 10000 { 112 | // See comment golang.org/issue/4556#c15. 113 | return dst, errors.New("time: year outside of range [0,9999]") 114 | } 115 | if opts.flags.has(unixTime) { 116 | return strconv.AppendInt(dst, t.Unix(), 10), nil 117 | } 118 | switch opts.timeLayout { 119 | case time.RFC3339: 120 | return appendRFC3339Time(t, dst, false), nil 121 | case time.RFC3339Nano: 122 | return appendRFC3339Time(t, dst, true), nil 123 | default: 124 | dst = append(dst, '"') 125 | dst = t.AppendFormat(dst, opts.timeLayout) 126 | dst = append(dst, '"') 127 | return dst, nil 128 | } 129 | } 130 | 131 | // encodeDuration appends the time.Duration value pointed 132 | // by p to dst based on the format configured in opts. 133 | func encodeDuration(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 134 | d := *(*time.Duration)(p) 135 | 136 | switch opts.durationFmt { 137 | default: // DurationNanoseconds 138 | return strconv.AppendInt(dst, d.Nanoseconds(), 10), nil 139 | case DurationMinutes: 140 | return appendFloat(dst, d.Minutes(), 64) 141 | case DurationSeconds: 142 | return appendFloat(dst, d.Seconds(), 64) 143 | case DurationMicroseconds: 144 | return strconv.AppendInt(dst, int64(d)/1e3, 10), nil 145 | case DurationMilliseconds: 146 | return strconv.AppendInt(dst, int64(d)/1e6, 10), nil 147 | case DurationString: 148 | dst = append(dst, '"') 149 | dst = appendDuration(dst, d) 150 | dst = append(dst, '"') 151 | return dst, nil 152 | } 153 | } 154 | 155 | func appendFloat(dst []byte, f float64, bs int) ([]byte, error) { 156 | if math.IsInf(f, 0) || math.IsNaN(f) { 157 | return dst, &UnsupportedValueError{ 158 | reflect.ValueOf(f), 159 | strconv.FormatFloat(f, 'g', -1, bs), 160 | } 161 | } 162 | // Convert as it was an ES6 number to string conversion. 163 | // This matches most other JSON generators. The following 164 | // code is taken from the floatEncoder implementation of 165 | // the encoding/json package of the Go standard library. 166 | abs := math.Abs(f) 167 | format := byte('f') 168 | if abs != 0 { 169 | if bs == 64 && (abs < 1e-6 || abs >= 1e21) || 170 | bs == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) { 171 | format = 'e' 172 | } 173 | } 174 | dst = strconv.AppendFloat(dst, f, format, -1, bs) 175 | if format == 'e' { 176 | n := len(dst) 177 | if n >= 4 && dst[n-4] == 'e' && dst[n-3] == '-' && dst[n-2] == '0' { 178 | dst[n-2] = dst[n-1] 179 | dst = dst[:n-1] 180 | } 181 | } 182 | return dst, nil 183 | } 184 | 185 | func encodePointer(p unsafe.Pointer, dst []byte, opts encOpts, ins instruction) ([]byte, error) { 186 | if p = *(*unsafe.Pointer)(p); p != nil { 187 | return ins(p, dst, opts) 188 | } 189 | return append(dst, "null"...), nil 190 | } 191 | 192 | func encodeStruct( 193 | p unsafe.Pointer, dst []byte, opts encOpts, flds []field, 194 | ) ([]byte, error) { 195 | var ( 196 | nxt = byte('{') 197 | key []byte // key of the field 198 | ) 199 | noHTMLEscape := opts.flags.has(noHTMLEscaping) 200 | 201 | fieldLoop: 202 | for i := 0; i < len(flds); i++ { 203 | f := &flds[i] // get pointer to prevent copy 204 | if opts.isDeniedField(f.name) { 205 | continue 206 | } 207 | fp := p 208 | 209 | // Find the nested struct field by following 210 | // the offset sequence, indirecting encountered 211 | // pointers as needed. 212 | for i := 0; i < len(f.embedSeq); i++ { 213 | s := &f.embedSeq[i] 214 | fp = unsafe.Pointer(uintptr(fp) + s.offset) 215 | if s.indir { 216 | if fp = *(*unsafe.Pointer)(fp); fp == nil { 217 | // When we encounter a nil pointer 218 | // in the chain, we have no choice 219 | // but to ignore the field. 220 | continue fieldLoop 221 | } 222 | } 223 | } 224 | // Ignore the field if it is a nil pointer and has 225 | // the omitnil option in his tag. 226 | if f.omitNil && *(*unsafe.Pointer)(fp) == nil { 227 | continue 228 | } 229 | // Ignore the field if it represents the zero-value 230 | // of its type and has the omitempty option in his tag. 231 | // Empty func is non-nil only if the field has the 232 | // omitempty option in its tag. 233 | if f.omitEmpty && f.empty(fp) { 234 | continue 235 | } 236 | key = f.keyEscHTML 237 | if noHTMLEscape { 238 | key = f.keyNonEsc 239 | } 240 | lastKeyOffset := len(dst) 241 | dst = append(dst, nxt) 242 | if nxt == '{' { 243 | lastKeyOffset++ 244 | } 245 | nxt = ',' 246 | dst = append(dst, key...) 247 | 248 | var err error 249 | if dst, err = f.instr(fp, dst, opts); err != nil { 250 | return dst, err 251 | } 252 | if f.omitNullMarshaler && len(dst) > 4 && bytes.Compare(dst[len(dst)-4:], []byte("null")) == 0 { 253 | dst = dst[:lastKeyOffset] 254 | } 255 | } 256 | if nxt == '{' { 257 | return append(dst, "{}"...), nil 258 | } 259 | return append(dst, '}'), nil 260 | } 261 | 262 | func encodeSlice( 263 | p unsafe.Pointer, dst []byte, opts encOpts, ins instruction, es uintptr, 264 | ) ([]byte, error) { 265 | shdr := (*sliceHeader)(p) 266 | if shdr.Data == nil { 267 | if opts.flags.has(nilSliceEmpty) { 268 | return append(dst, "[]"...), nil 269 | } 270 | return append(dst, "null"...), nil 271 | } 272 | if shdr.Len == 0 { 273 | return append(dst, "[]"...), nil 274 | } 275 | return encodeArray(shdr.Data, dst, opts, ins, es, shdr.Len, false) 276 | } 277 | 278 | // encodeByteSlice appends a byte slice to dst as 279 | // a JSON string. If the options flag rawByteSlice 280 | // is set, the escaped bytes are appended to the 281 | // buffer directly, otherwise in base64 form. 282 | // nolint:unparam 283 | func encodeByteSlice(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 284 | b := *(*[]byte)(p) 285 | if b == nil { 286 | return append(dst, "null"...), nil 287 | } 288 | dst = append(dst, '"') 289 | 290 | if opts.flags.has(rawByteSlice) { 291 | dst = appendEscapedBytes(dst, b, opts) 292 | } else { 293 | n := base64.StdEncoding.EncodedLen(len(b)) 294 | if a := cap(dst) - len(dst); a < n { 295 | new := make([]byte, cap(dst)+(n-a)) 296 | copy(new, dst) 297 | dst = new[:len(dst)] 298 | } 299 | end := len(dst) + n 300 | base64.StdEncoding.Encode(dst[len(dst):end], b) 301 | 302 | dst = dst[:end] 303 | } 304 | return append(dst, '"'), nil 305 | } 306 | 307 | func encodeArray( 308 | p unsafe.Pointer, dst []byte, opts encOpts, ins instruction, es uintptr, len int, isByteArray bool, 309 | ) ([]byte, error) { 310 | if isByteArray && opts.flags.has(byteArrayAsString) { 311 | return encodeByteArrayAsString(p, dst, opts, len), nil 312 | } 313 | var err error 314 | nxt := byte('[') 315 | 316 | for i := 0; i < len; i++ { 317 | dst = append(dst, nxt) 318 | nxt = ',' 319 | v := unsafe.Pointer(uintptr(p) + (uintptr(i) * es)) 320 | if dst, err = ins(v, dst, opts); err != nil { 321 | return dst, err 322 | } 323 | } 324 | if nxt == '[' { 325 | return append(dst, "[]"...), nil 326 | } 327 | return append(dst, ']'), nil 328 | } 329 | 330 | // encodeByteArrayAsString appends the escaped 331 | // bytes of the byte array pointed by p to dst 332 | // as a JSON string. 333 | func encodeByteArrayAsString(p unsafe.Pointer, dst []byte, opts encOpts, len int) []byte { 334 | // For byte type, size is guaranteed to be 1, 335 | // so the slice length is the same as the array's. 336 | // see golang.org/ref/spec#Size_and_alignment_guarantees 337 | b := *(*[]byte)(unsafe.Pointer(&sliceHeader{ 338 | Data: p, 339 | Len: len, 340 | Cap: len, 341 | })) 342 | dst = append(dst, '"') 343 | dst = appendEscapedBytes(dst, b, opts) 344 | dst = append(dst, '"') 345 | 346 | return dst 347 | } 348 | 349 | func encodeMap( 350 | p unsafe.Pointer, dst []byte, opts encOpts, t reflect.Type, ki, vi instruction, 351 | ) ([]byte, error) { 352 | m := *(*unsafe.Pointer)(p) 353 | if m == nil { 354 | if opts.flags.has(nilMapEmpty) { 355 | return append(dst, "{}"...), nil 356 | } 357 | return append(dst, "null"...), nil 358 | } 359 | ml := maplen(m) 360 | if ml == 0 { 361 | return append(dst, "{}"...), nil 362 | } 363 | dst = append(dst, '{') 364 | 365 | rt := unpackEface(t).word 366 | it := newHiter(rt, m) 367 | 368 | var err error 369 | if opts.flags.has(unsortedMap) { 370 | dst, err = encodeUnsortedMap(it, dst, opts, ki, vi) 371 | } else { 372 | dst, err = encodeSortedMap(it, dst, opts, ki, vi, ml) 373 | } 374 | hiterPool.Put(it) 375 | 376 | if err != nil { 377 | return dst, err 378 | } 379 | return append(dst, '}'), err 380 | } 381 | 382 | // encodeUnsortedMap appends the elements of the map 383 | // pointed by p as comma-separated k/v pairs to dst, 384 | // in unspecified order. 385 | func encodeUnsortedMap( 386 | it *hiter, dst []byte, opts encOpts, ki, vi instruction, 387 | ) ([]byte, error) { 388 | var ( 389 | n int 390 | err error 391 | ) 392 | for ; it.key != nil; mapiternext(it) { 393 | if n != 0 { 394 | dst = append(dst, ',') 395 | } 396 | // Encode entry's key. 397 | if dst, err = ki(it.key, dst, opts); err != nil { 398 | return dst, err 399 | } 400 | dst = append(dst, ':') 401 | 402 | // Encode entry's value. 403 | if dst, err = vi(it.val, dst, opts); err != nil { 404 | return dst, err 405 | } 406 | n++ 407 | } 408 | return dst, nil 409 | } 410 | 411 | // encodeUnsortedMap appends the elements of the map 412 | // pointed by p as comma-separated k/v pairs to dst, 413 | // sorted by key in lexicographical order. 414 | func encodeSortedMap( 415 | it *hiter, dst []byte, opts encOpts, ki, vi instruction, ml int, 416 | ) ([]byte, error) { 417 | var ( 418 | off int 419 | err error 420 | buf = cachedBuffer() 421 | mel *mapElems 422 | ) 423 | if v := mapElemsPool.Get(); v != nil { 424 | mel = v.(*mapElems) 425 | } else { 426 | mel = &mapElems{s: make([]kv, 0, ml)} 427 | } 428 | for ; it.key != nil; mapiternext(it) { 429 | kv := kv{} 430 | 431 | // Encode the key and store the buffer 432 | // portion to use during sort. 433 | if buf.B, err = ki(it.key, buf.B, opts); err != nil { 434 | break 435 | } 436 | // Omit quotes of keys. 437 | kv.key = buf.B[off+1 : len(buf.B)-1] 438 | 439 | // Add separator after key. 440 | buf.B = append(buf.B, ':') 441 | 442 | // Encode the value and store the buffer 443 | // portion corresponding to the semicolon 444 | // delimited key/value pair. 445 | if buf.B, err = vi(it.val, buf.B, opts); err != nil { 446 | break 447 | } 448 | kv.keyval = buf.B[off:len(buf.B)] 449 | mel.s = append(mel.s, kv) 450 | off = len(buf.B) 451 | } 452 | if err == nil { 453 | // Sort map entries by key in 454 | // lexicographical order. 455 | sort.Sort(mel) 456 | 457 | // Append sorted comma-delimited k/v 458 | // pairs to the given buffer. 459 | for i, kv := range mel.s { 460 | if i != 0 { 461 | dst = append(dst, ',') 462 | } 463 | dst = append(dst, kv.keyval...) 464 | } 465 | } 466 | // The map elements must be released before 467 | // the buffer, because each k/v pair holds 468 | // two sublices that points to the buffer's 469 | // backing array. 470 | releaseMapElems(mel) 471 | bufferPool.Put(buf) 472 | 473 | return dst, err 474 | } 475 | 476 | // encodeSyncMap appends the elements of a sync.Map pointed 477 | // to by p to dst and returns the extended buffer. 478 | // This function replicates the behavior of encoding Go maps, 479 | // by returning an error for keys that are not of type string 480 | // or int, or that does not implement encoding.TextMarshaler. 481 | func encodeSyncMap(p unsafe.Pointer, dst []byte, opts encOpts) ([]byte, error) { 482 | sm := (*sync.Map)(p) 483 | dst = append(dst, '{') 484 | 485 | // The sync.Map type does not have a Len() method to 486 | // determine if it has no entries, to bail out early, 487 | // so we just range over it to encode all available 488 | // entries. 489 | // If an error arises while encoding a key or a value, 490 | // the error is stored and the method used by Range() 491 | // returns false to stop the map's iteration. 492 | var err error 493 | if opts.flags.has(unsortedMap) { 494 | dst, err = encodeUnsortedSyncMap(sm, dst, opts) 495 | } else { 496 | dst, err = encodeSortedSyncMap(sm, dst, opts) 497 | } 498 | if err != nil { 499 | return dst, err 500 | } 501 | return append(dst, '}'), nil 502 | } 503 | 504 | // encodeUnsortedSyncMap is similar to encodeUnsortedMap 505 | // but operates on a sync.Map type instead of a Go map. 506 | func encodeUnsortedSyncMap(sm *sync.Map, dst []byte, opts encOpts) ([]byte, error) { 507 | var ( 508 | n int 509 | err error 510 | ) 511 | sm.Range(func(key, value interface{}) bool { 512 | if n != 0 { 513 | dst = append(dst, ',') 514 | } 515 | // Encode the key. 516 | if dst, err = appendSyncMapKey(dst, key, opts); err != nil { 517 | return false 518 | } 519 | dst = append(dst, ':') 520 | 521 | // Encode the value. 522 | if dst, err = appendJSON(dst, value, opts); err != nil { 523 | return false 524 | } 525 | n++ 526 | return true 527 | }) 528 | return dst, err 529 | } 530 | 531 | // encodeSortedSyncMap is similar to encodeSortedMap 532 | // but operates on a sync.Map type instead of a Go map. 533 | func encodeSortedSyncMap(sm *sync.Map, dst []byte, opts encOpts) ([]byte, error) { 534 | var ( 535 | off int 536 | err error 537 | buf = cachedBuffer() 538 | mel *mapElems 539 | ) 540 | if v := mapElemsPool.Get(); v != nil { 541 | mel = v.(*mapElems) 542 | } else { 543 | mel = &mapElems{s: make([]kv, 0)} 544 | } 545 | sm.Range(func(key, value interface{}) bool { 546 | kv := kv{} 547 | 548 | // Encode the key and store the buffer 549 | // portion to use during the later sort. 550 | if buf.B, err = appendSyncMapKey(buf.B, key, opts); err != nil { 551 | return false 552 | } 553 | // Omit quotes of keys. 554 | kv.key = buf.B[off+1 : len(buf.B)-1] 555 | 556 | // Add separator after key. 557 | buf.B = append(buf.B, ':') 558 | 559 | // Encode the value and store the buffer 560 | // portion corresponding to the semicolon 561 | // delimited key/value pair. 562 | if buf.B, err = appendJSON(buf.B, value, opts); err != nil { 563 | return false 564 | } 565 | kv.keyval = buf.B[off:len(buf.B)] 566 | mel.s = append(mel.s, kv) 567 | off = len(buf.B) 568 | 569 | return true 570 | }) 571 | if err == nil { 572 | // Sort map entries by key in 573 | // lexicographical order. 574 | sort.Sort(mel) 575 | 576 | // Append sorted comma-delimited k/v 577 | // pairs to the given buffer. 578 | for i, kv := range mel.s { 579 | if i != 0 { 580 | dst = append(dst, ',') 581 | } 582 | dst = append(dst, kv.keyval...) 583 | } 584 | } 585 | releaseMapElems(mel) 586 | bufferPool.Put(buf) 587 | 588 | return dst, err 589 | } 590 | 591 | func appendSyncMapKey(dst []byte, key interface{}, opts encOpts) ([]byte, error) { 592 | if key == nil { 593 | return dst, errors.New("unsupported nil key in sync.Map") 594 | } 595 | kt := reflect.TypeOf(key) 596 | var ( 597 | isStr = isString(kt) 598 | isInt = isInteger(kt) 599 | isTxt = kt.Implements(textMarshalerType) 600 | ) 601 | if !isStr && !isInt && !isTxt { 602 | return dst, fmt.Errorf("unsupported key of type %s in sync.Map", kt) 603 | } 604 | var err error 605 | 606 | // Quotes the key if the type is not 607 | // encoded with quotes by default. 608 | quoted := !isStr && !isTxt 609 | 610 | // Ensure map key precedence for keys of type 611 | // string by using the encodeString function 612 | // directly instead of the generic appendJSON. 613 | if isStr { 614 | dst, err = encodeString(unpackEface(key).word, dst, opts) 615 | runtime.KeepAlive(key) 616 | } else { 617 | if quoted { 618 | dst = append(dst, '"') 619 | } 620 | dst, err = appendJSON(dst, key, opts) 621 | } 622 | if err != nil { 623 | return dst, err 624 | } 625 | if quoted { 626 | dst = append(dst, '"') 627 | } 628 | return dst, nil 629 | } 630 | 631 | func encodeMarshaler( 632 | p unsafe.Pointer, dst []byte, opts encOpts, t reflect.Type, canAddr bool, fn marshalerEncodeFunc, 633 | ) ([]byte, error) { 634 | // The content of this function and packEface 635 | // is similar to the following code using the 636 | // reflect package. 637 | // 638 | // v := reflect.NewAt(t, p) 639 | // if !canAddr { 640 | // v = v.Elem() 641 | // k := v.Kind() 642 | // if (k == reflect.Ptr || k == reflect.Interface) && v.IsNil() { 643 | // return append(dst, "null"...), nil 644 | // } 645 | // } else if v.IsNil() { 646 | // return append(dst, "null"...), nil 647 | // } 648 | // return fn(v.Interface(), dst, opts, t) 649 | // 650 | if !canAddr { 651 | if t.Kind() == reflect.Ptr || t.Kind() == reflect.Interface { 652 | if *(*unsafe.Pointer)(p) == nil { 653 | return append(dst, "null"...), nil 654 | } 655 | } 656 | } else { 657 | if p == nil { 658 | return append(dst, "null"...), nil 659 | } 660 | t = reflect.PtrTo(t) 661 | } 662 | var i interface{} 663 | 664 | if t.Kind() == reflect.Interface { 665 | // Special case: return the element inside the 666 | // interface. The empty interface has one layout, 667 | // all interfaces with methods have another one. 668 | if t.NumMethod() == 0 { 669 | i = *(*interface{})(p) 670 | } else { 671 | i = *(*interface{ M() })(p) 672 | } 673 | } else { 674 | i = packEface(p, t, t.Kind() == reflect.Ptr && !canAddr) 675 | } 676 | return fn(i, dst, opts, t) 677 | } 678 | 679 | func encodeAppendMarshalerCtx( 680 | i interface{}, dst []byte, opts encOpts, t reflect.Type, 681 | ) ([]byte, error) { 682 | dst2, err := i.(AppendMarshalerCtx).AppendJSONContext(opts.ctx, dst) 683 | if err != nil { 684 | return dst, &MarshalerError{t, err, marshalerAppendJSONCtx} 685 | } 686 | return dst2, nil 687 | } 688 | 689 | func encodeAppendMarshaler( 690 | i interface{}, dst []byte, _ encOpts, t reflect.Type, 691 | ) ([]byte, error) { 692 | dst2, err := i.(AppendMarshaler).AppendJSON(dst) 693 | if err != nil { 694 | return dst, &MarshalerError{t, err, marshalerAppendJSON} 695 | } 696 | return dst2, nil 697 | } 698 | 699 | func encodeJSONMarshaler(i interface{}, dst []byte, opts encOpts, t reflect.Type) ([]byte, error) { 700 | b, err := i.(json.Marshaler).MarshalJSON() 701 | if err != nil { 702 | return dst, &MarshalerError{t, err, marshalerJSON} 703 | } 704 | if opts.flags.has(noCompact) { 705 | return append(dst, b...), nil 706 | } 707 | // This is redundant with the parsing done 708 | // by appendCompactJSON, but for the time 709 | // being, we can't use the scanner of the 710 | // standard library. 711 | if !json.Valid(b) { 712 | return dst, &MarshalerError{t, &SyntaxError{ 713 | msg: "json: invalid value", 714 | }, marshalerJSON} 715 | } 716 | return appendCompactJSON(dst, b, !opts.flags.has(noHTMLEscaping)) 717 | } 718 | 719 | func encodeTextMarshaler(i interface{}, dst []byte, _ encOpts, t reflect.Type) ([]byte, error) { 720 | b, err := i.(encoding.TextMarshaler).MarshalText() 721 | if err != nil { 722 | return dst, &MarshalerError{t, err, marshalerText} 723 | } 724 | dst = append(dst, '"') 725 | dst = append(dst, b...) 726 | dst = append(dst, '"') 727 | 728 | return dst, nil 729 | } 730 | 731 | // appendCompactJSON appends to dst the JSON-encoded src 732 | // with insignificant space characters elided. If escHTML 733 | // is true, HTML-characters are also escaped. 734 | func appendCompactJSON(dst, src []byte, escHTML bool) ([]byte, error) { 735 | var ( 736 | inString bool 737 | skipNext bool 738 | ) 739 | at := 0 // accumulated bytes start index 740 | 741 | for i, c := range src { 742 | if escHTML { 743 | // Escape HTML characters. 744 | if c == '<' || c == '>' || c == '&' { 745 | if at < i { 746 | dst = append(dst, src[at:i]...) 747 | } 748 | dst = append(dst, `\u00`...) 749 | dst = append(dst, hex[c>>4], hex[c&0xF]) 750 | at = i + 1 751 | continue 752 | } 753 | } 754 | // Convert U+2028 and U+2029. 755 | // (E2 80 A8 and E2 80 A9). 756 | if c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 { 757 | if at < i { 758 | dst = append(dst, src[at:i]...) 759 | } 760 | dst = append(dst, `\u202`...) 761 | dst = append(dst, hex[src[i+2]&0xF]) 762 | at = i + 3 763 | continue 764 | } 765 | if !inString { 766 | switch c { 767 | case '"': 768 | // Within a string, we don't elide 769 | // insignificant space characters. 770 | inString = true 771 | case ' ', '\n', '\r', '\t': 772 | // Append the accumulated bytes, 773 | // and skip the current character. 774 | if at < i { 775 | dst = append(dst, src[at:i]...) 776 | } 777 | at = i + 1 778 | } 779 | continue 780 | } 781 | // Next character is escaped, and must 782 | // not be interpreted as the end of a 783 | // string by mistake. 784 | if skipNext { 785 | skipNext = false 786 | continue 787 | } 788 | // Next character must be skipped. 789 | if c == '\\' { 790 | skipNext = true 791 | continue 792 | } 793 | // Leaving a string value. 794 | if c == '"' { 795 | inString = false 796 | } 797 | } 798 | if at < len(src) { 799 | dst = append(dst, src[at:]...) 800 | } 801 | return dst, nil 802 | } 803 | 804 | func appendEscapedBytes(dst []byte, b []byte, opts encOpts) []byte { 805 | if opts.flags.has(noStringEscaping) { 806 | return append(dst, b...) 807 | } 808 | var ( 809 | i = 0 810 | at = 0 811 | ) 812 | noCoerce := opts.flags.has(noUTF8Coercion) 813 | noEscape := opts.flags.has(noHTMLEscaping) 814 | 815 | for i < len(b) { 816 | if c := b[i]; c < utf8.RuneSelf { 817 | // Check whether c can be used in a JSON string 818 | // without escaping, or it is a problematic HTML 819 | // character. 820 | if c >= ' ' && c != '\\' && c != '"' && (noEscape || (c != '<' && c != '>' && c != '&')) { 821 | // If the current character doesn't need 822 | // to be escaped, accumulate the bytes to 823 | // save some operations. 824 | i++ 825 | continue 826 | } 827 | // Write accumulated single-byte characters. 828 | if at < i { 829 | dst = append(dst, b[at:i]...) 830 | } 831 | // The encoding/json package implements only 832 | // a few of the special two-character escape 833 | // sequence described in the RFC 8259, Section 7. 834 | // \b and \f were ignored on purpose, see 835 | // https://codereview.appspot.com/4678046. 836 | switch c { 837 | case '"', '\\': 838 | dst = append(dst, '\\', c) 839 | case '\n': // 0xA, line feed 840 | dst = append(dst, '\\', 'n') 841 | case '\r': // 0xD, carriage return 842 | dst = append(dst, '\\', 'r') 843 | case '\t': // 0x9, horizontal tab 844 | dst = append(dst, '\\', 't') 845 | default: 846 | dst = append(dst, `\u00`...) 847 | dst = append(dst, hex[c>>4]) 848 | dst = append(dst, hex[c&0xF]) 849 | } 850 | i++ 851 | at = i 852 | continue 853 | } 854 | r, size := utf8.DecodeRune(b[i:]) 855 | 856 | if !noCoerce { 857 | // Coerce to valid UTF-8, by replacing invalid 858 | // bytes with the Unicode replacement rune. 859 | if r == utf8.RuneError && size == 1 { 860 | if at < i { 861 | dst = append(dst, b[at:i]...) 862 | } 863 | dst = append(dst, `\ufffd`...) 864 | i += size 865 | at = i 866 | continue 867 | } 868 | // U+2028 is LINE SEPARATOR. 869 | // U+2029 is PARAGRAPH SEPARATOR. 870 | // They are both technically valid characters in 871 | // JSON strings, but don't work in JSONP, which has 872 | // to be evaluated as JavaScript, and can lead to 873 | // security holes there. It is valid JSON to escape 874 | // them, so we do so unconditionally. 875 | // See http://timelessrepo.com/json-isnt-a-javascript-subset. 876 | if r == '\u2028' || r == '\u2029' { 877 | if at < i { 878 | dst = append(dst, b[at:i]...) 879 | } 880 | dst = append(dst, `\u202`...) 881 | dst = append(dst, hex[r&0xF]) 882 | i += size 883 | at = i 884 | continue 885 | } 886 | i += size 887 | continue 888 | } 889 | i += size 890 | } 891 | if at < len(b) { 892 | dst = append(dst, b[at:]...) 893 | } 894 | return dst 895 | } 896 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package jettison 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "math" 11 | "math/big" 12 | "net" 13 | "reflect" 14 | "regexp" 15 | "strconv" 16 | "sync" 17 | "testing" 18 | "time" 19 | ) 20 | 21 | type ( 22 | jmr string 23 | jmv []string 24 | ) 25 | 26 | func (*jmr) MarshalJSON() ([]byte, error) { return []byte(`"XYZ"`), nil } 27 | func (jmv) MarshalJSON() ([]byte, error) { return []byte(`"ZYX"`), nil } 28 | 29 | type ( 30 | mapss struct { 31 | M map[string]string 32 | } 33 | inner struct { 34 | M map[string]string 35 | } 36 | outer struct { 37 | M map[string]inner 38 | } 39 | z struct { 40 | S []byte 41 | } 42 | y struct { 43 | P float64 `json:"p,omitempty"` 44 | Q uint64 `json:"q,omitempty"` 45 | R uint8 46 | } 47 | x struct { 48 | A *string `json:"a,string"` 49 | B1 int64 `json:"b1,string"` 50 | B2 uint16 `json:"b2"` 51 | C *bool `json:"c,string"` 52 | D float32 53 | E1 *[]int 54 | E2 []string 55 | E3 []jmr 56 | F1 [4]string 57 | F2 [1]jmr 58 | F3 *[1]jmr 59 | G1 map[int]*string 60 | G2 map[string]*map[string]string 61 | G3 map[int]map[string]map[int]string 62 | G4 map[string]mapss 63 | G5 outer 64 | G6 map[int]jmr 65 | G7 map[int]*jmr 66 | G8 map[int]bool `json:",omitempty"` 67 | H1 jmr 68 | H2 *jmr 69 | H3 jmv 70 | H4 *jmv 71 | I time.Time 72 | J time.Duration 73 | K json.Number 74 | L json.RawMessage 75 | M1 interface{} 76 | M2 interface{} 77 | N struct{} 78 | X *x 79 | *y 80 | z `json:"z"` 81 | } 82 | ) 83 | 84 | var ( 85 | s = "Loreum" 86 | b = true 87 | m = map[string]string{"b": "c"} 88 | xx = x{ 89 | A: &s, 90 | B1: -42, 91 | B2: 42, 92 | C: &b, 93 | D: math.MaxFloat32, 94 | E1: &[]int{1, 2, 3}, 95 | E2: []string{"x", "y", "z"}, 96 | E3: []jmr{"1"}, 97 | F1: [4]string{"a", "b", "c", "d"}, 98 | F2: [1]jmr{"1"}, 99 | F3: &[1]jmr{"1"}, 100 | G1: map[int]*string{2: &s, 3: new(string)}, 101 | G2: map[string]*map[string]string{"a": &m}, 102 | G3: map[int]map[string]map[int]string{1: {"a": {2: "b"}}}, 103 | G4: map[string]mapss{"1": {M: map[string]string{"2": "3"}}}, 104 | G5: outer{map[string]inner{"outer": {map[string]string{"key": "val"}}}}, 105 | G6: map[int]jmr{1: "jmr"}, 106 | G7: map[int]*jmr{1: new(jmr)}, 107 | G8: map[int]bool{}, 108 | H1: "jmp", 109 | H2: nil, 110 | H3: nil, 111 | H4: nil, 112 | I: time.Now(), 113 | J: 3 * time.Minute, 114 | K: "3.14", 115 | L: []byte(`{ "a":"b" }`), 116 | M1: uint32(255), 117 | M2: &s, 118 | X: &x{H1: "jmv"}, 119 | y: &y{R: math.MaxUint8}, 120 | z: z{S: []byte("Loreum")}, 121 | } 122 | ) 123 | 124 | // marshalCompare compares the JSON encoding 125 | // of v between Jettison and encoding/json. 126 | func marshalCompare(t *testing.T, v interface{}, name string) { 127 | jb1, err := Marshal(v) 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | jb2, err := MarshalOpts(v) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | if !bytes.Equal(jb1, jb2) { 136 | t.Error("non-equal outputs for Marshal and MarshalOpts") 137 | } 138 | jb3, err := Append([]byte(nil), v) 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | if !bytes.Equal(jb1, jb3) { 143 | t.Error("non-equal outputs for Marshal and Append") 144 | } 145 | sb, err := json.Marshal(v) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | t.Logf("standard: %s", string(sb)) 150 | t.Logf("jettison: %s", string(jb1)) 151 | 152 | if !bytes.Equal(jb1, sb) { 153 | t.Errorf("%s: non-equal outputs", name) 154 | } 155 | } 156 | 157 | func marshalCompareError(t *testing.T, v interface{}, name string) { 158 | _, errj := Marshal(v) 159 | if errj == nil { 160 | t.Fatalf("expected non-nil error") 161 | } 162 | _, errs := json.Marshal(v) 163 | if errs == nil { 164 | t.Fatalf("expected non-nil error") 165 | } 166 | t.Logf("standard: %s", errs) 167 | t.Logf("jettison: %s", errj) 168 | 169 | if errs.Error() != errj.Error() { 170 | t.Errorf("%s: non-equal outputs", name) 171 | } 172 | } 173 | 174 | func TestAll(t *testing.T) { 175 | marshalCompare(t, nil, "nil") 176 | marshalCompare(t, xx, "non-pointer") 177 | marshalCompare(t, &xx, "pointer") 178 | } 179 | 180 | func TestInvalidEncodeOpts(t *testing.T) { 181 | for _, opt := range []Option{ 182 | TimeLayout(""), 183 | DurationFormat(DurationFmt(-1)), 184 | DurationFormat(DurationFmt(6)), 185 | WithContext(nil), // nolint:staticcheck 186 | } { 187 | _, err1 := MarshalOpts(struct{}{}, opt) 188 | _, err2 := AppendOpts([]byte(nil), struct{}{}, opt) 189 | 190 | for _, err := range []error{err1, err2} { 191 | if err != nil { 192 | e, ok := err.(*InvalidOptionError) 193 | if !ok { 194 | t.Errorf("got %T, want InvalidOptionError", err) 195 | } 196 | if e.Error() == "" { 197 | t.Errorf("expected non-empty error message") 198 | } 199 | } else { 200 | t.Error("expected non-nil error") 201 | } 202 | } 203 | } 204 | } 205 | 206 | // TestBasicTypes tests the marshaling of basic types. 207 | func TestBasicTypes(t *testing.T) { 208 | testdata := []interface{}{ 209 | true, 210 | false, 211 | "Loreum", 212 | int8(math.MaxInt8), 213 | int16(math.MaxInt16), 214 | int32(math.MaxInt32), 215 | int64(math.MaxInt64), 216 | uint8(math.MaxUint8), 217 | uint16(math.MaxUint16), 218 | uint32(math.MaxUint32), 219 | uint64(math.MaxUint64), 220 | uintptr(0xBEEF), 221 | (*bool)(nil), 222 | (*int)(nil), 223 | (*string)(nil), 224 | } 225 | for _, v := range testdata { 226 | marshalCompare(t, v, "") 227 | } 228 | } 229 | 230 | // TestCompositeTypes tests the marshaling of composite types. 231 | func TestCompositeTypes(t *testing.T) { 232 | var ( 233 | jmref = jmr("jmr") 234 | jmval = jmv([]string{"a", "b", "c"}) 235 | ) 236 | testdata := []interface{}{ 237 | []uint{}, 238 | []int{1, 2, 3}, 239 | []int(nil), 240 | (*[]int)(nil), 241 | []string{"a", "b", "c"}, 242 | [2]bool{true, false}, 243 | (*[4]string)(nil), 244 | map[string]int{"a": 1, "b": 2}, 245 | &map[int]string{1: "a", 2: "b"}, 246 | (map[string]int)(nil), 247 | time.Now(), 248 | 3*time.Minute + 35*time.Second, 249 | jmref, 250 | &jmref, 251 | jmval, 252 | &jmval, 253 | } 254 | for _, v := range testdata { 255 | marshalCompare(t, v, "") 256 | } 257 | } 258 | 259 | // TestUnsupportedTypes tests that marshaling an 260 | // unsupported type such as channel, complex, and 261 | // function value returns an UnsupportedTypeError. 262 | // The error message is compared with the one that 263 | // is returned by json.Marshal. 264 | func TestUnsupportedTypes(t *testing.T) { 265 | testdata := []interface{}{ 266 | make(chan int), 267 | func() {}, 268 | complex64(0), 269 | complex128(0), 270 | make([]chan int, 1), 271 | [1]complex64{}, 272 | &[1]complex128{}, 273 | map[int]chan bool{1: make(chan bool)}, 274 | struct{ F func() }{func() {}}, 275 | &struct{ C complex64 }{0}, 276 | } 277 | for _, v := range testdata { 278 | marshalCompareError(t, v, "") 279 | } 280 | } 281 | 282 | // TestInvalidFloatValues tests that encoding an 283 | // invalid float value returns UnsupportedValueError. 284 | func TestInvalidFloatValues(t *testing.T) { 285 | for _, v := range []float64{ 286 | math.NaN(), 287 | math.Inf(-1), 288 | math.Inf(1), 289 | } { 290 | _, err := Marshal(v) 291 | if err != nil { 292 | if _, ok := err.(*UnsupportedValueError); !ok { 293 | t.Errorf("got %T, want UnsupportedValueError", err) 294 | } 295 | } else { 296 | t.Error("got nil, want non-nil error") 297 | } 298 | // Error message must be the same as 299 | // the one of the standard library. 300 | marshalCompareError(t, v, "") 301 | } 302 | } 303 | 304 | // TestJSONNumber tests that a json.Number literal value 305 | // can be marshaled, and that an error is returned if it 306 | // isn't a valid number according to the JSON grammar. 307 | func TestJSONNumber(t *testing.T) { 308 | valid := []json.Number{ 309 | "42", 310 | "-42", 311 | "24.42", 312 | "-666.66", 313 | "3.14", 314 | "-3.14", 315 | "1e3", 316 | "1E-6", 317 | "1E+42", 318 | // Special case to keep backward 319 | // compatibility with Go1.5, that 320 | // encodes the empty string as "0". 321 | "", 322 | } 323 | for _, v := range valid { 324 | marshalCompare(t, v, "valid") 325 | } 326 | invalid := []json.Number{ 327 | "1E+4.0", 328 | "084", 329 | "-03.14", 330 | "-", 331 | "invalid", 332 | } 333 | for _, v := range invalid { 334 | marshalCompareError(t, v, "invalid") 335 | } 336 | } 337 | 338 | func TestInvalidTime(t *testing.T) { 339 | // Special case to test error when the year 340 | // of the date is outside of range [0.9999]. 341 | // see golang.org/issue/4556#c15. 342 | for _, tm := range []time.Time{ 343 | time.Date(-1, time.January, 1, 0, 0, 0, 0, time.UTC), 344 | time.Date(10000, time.January, 1, 0, 0, 0, 0, time.UTC), 345 | } { 346 | _, err := Marshal(tm) 347 | if err != nil { 348 | want := "time: year outside of range [0,9999]" 349 | if err.Error() != want { 350 | t.Errorf("got %q, want %q", err.Error(), want) 351 | } 352 | } else { 353 | t.Error("got nil, want non-nil error") 354 | } 355 | } 356 | } 357 | 358 | // TestRenamedByteSlice tests that a name type 359 | // that represents a slice of bytes is marshaled 360 | // the same way as a regular byte slice. 361 | func TestRenamedByteSlice(t *testing.T) { 362 | type ( 363 | b byte 364 | b1 []byte 365 | b2 []b 366 | ) 367 | testdata := []interface{}{ 368 | b1("byte slice 1"), 369 | b2("byte slice 2"), 370 | } 371 | for _, v := range testdata { 372 | marshalCompare(t, v, "") 373 | } 374 | } 375 | 376 | func TestByteSliceSizes(t *testing.T) { 377 | makeSlice := func(size int) []byte { 378 | b := make([]byte, size) 379 | if _, err := rand.Read(b); err != nil { 380 | t.Fatal(err) 381 | } 382 | return b 383 | } 384 | for _, v := range []interface{}{ 385 | makeSlice(0), 386 | makeSlice(1024), 387 | makeSlice(2048), 388 | makeSlice(4096), 389 | makeSlice(8192), 390 | } { 391 | marshalCompare(t, v, "") 392 | } 393 | } 394 | 395 | // TestSortedSyncMap tests the marshaling 396 | // of a sorted sync.Map value. 397 | func TestSortedSyncMap(t *testing.T) { 398 | var sm sync.Map 399 | 400 | sm.Store(1, "one") 401 | sm.Store("a", 42) 402 | sm.Store("b", false) 403 | sm.Store(mkvstrMarshaler("c"), -42) 404 | sm.Store(mkrstrMarshaler("d"), true) 405 | sm.Store(mkvintMarshaler(42), 1) 406 | sm.Store(mkrintMarshaler(42), 2) 407 | 408 | b, err := Marshal(&sm) 409 | if err != nil { 410 | t.Fatal(err) 411 | } 412 | want := `{"1":"one","42":2,"MKVINT":1,"a":42,"b":false,"c":-42,"d":true}` 413 | 414 | if !bytes.Equal(b, []byte(want)) { 415 | t.Errorf("got %#q, want %#q", b, want) 416 | } 417 | } 418 | 419 | // TestUnsortedSyncMap tests the marshaling 420 | // of an unsorted sync.Map value. 421 | func TestUnsortedSyncMap(t *testing.T) { 422 | // entries maps each interface k/v 423 | // pair to the string representation 424 | // of the key in payload. 425 | entries := map[string]struct { 426 | key interface{} 427 | val interface{} 428 | }{ 429 | "1": {1, "one"}, 430 | "a": {"a", 42}, 431 | "b": {"b", false}, 432 | "c": {mkvstrMarshaler("c"), -42}, 433 | "d": {mkrstrMarshaler("d"), true}, 434 | "MKVINT": {mkvintMarshaler(42), 1}, 435 | "42": {mkrintMarshaler(42), 2}, 436 | } 437 | var sm sync.Map 438 | for _, e := range entries { 439 | sm.Store(e.key, e.val) 440 | } 441 | bts, err := MarshalOpts(&sm, UnsortedMap()) 442 | if err != nil { 443 | t.Fatal(err) 444 | } 445 | m := make(map[string]interface{}) 446 | if err := json.Unmarshal(bts, &m); err != nil { 447 | t.Fatal(err) 448 | } 449 | // Unmarshaled map must contain exactly the 450 | // number of entries added to the sync map. 451 | if g, w := len(m), len(entries); g != w { 452 | t.Errorf("invalid lengths: got %d, want %d", g, w) 453 | } 454 | for k, v := range m { 455 | // Compare the marshaled representation 456 | // of each value to avoid false-positive 457 | // between integer and float types. 458 | b1, err1 := json.Marshal(v) 459 | b2, err2 := json.Marshal(entries[k].val) 460 | if err1 != nil { 461 | t.Fatal(err) 462 | } 463 | if err2 != nil { 464 | t.Fatal(err2) 465 | } 466 | if !bytes.Equal(b1, b2) { 467 | t.Errorf("for key %s: got %v, want %v", k, b1, b2) 468 | } 469 | } 470 | } 471 | 472 | // TestInvalidSyncMapKeys tests that marshaling a 473 | // sync.Map with unsupported key types returns an 474 | // error. 475 | func TestInvalidSyncMapKeys(t *testing.T) { 476 | testInvalidSyncMapKeys(t, true) 477 | testInvalidSyncMapKeys(t, false) 478 | } 479 | 480 | func testInvalidSyncMapKeys(t *testing.T, sorted bool) { 481 | for _, f := range []func(sm *sync.Map){ 482 | func(sm *sync.Map) { sm.Store(false, nil) }, 483 | func(sm *sync.Map) { sm.Store(new(int), nil) }, 484 | func(sm *sync.Map) { sm.Store(nil, nil) }, 485 | } { 486 | var ( 487 | sm sync.Map 488 | err error 489 | ) 490 | f(&sm) // add entries to sm 491 | if sorted { 492 | _, err = Marshal(&sm) 493 | } else { 494 | _, err = MarshalOpts(&sm, UnsortedMap()) 495 | } 496 | if err == nil { 497 | t.Error("expected a non-nil error") 498 | } 499 | } 500 | } 501 | 502 | // TestCompositeMapValue tests the marshaling 503 | // of maps with composite values. 504 | func TestCompositeMapValue(t *testing.T) { 505 | type x struct { 506 | A string `json:"a"` 507 | B int `json:"b"` 508 | C bool `json:"c"` 509 | } 510 | type y []uint32 511 | 512 | for _, v := range []interface{}{ 513 | map[string]x{ 514 | "1": {A: "A", B: 42, C: true}, 515 | "2": {A: "A", B: 84, C: false}, 516 | }, 517 | map[string]y{ 518 | "3": {7, 8, 9}, 519 | "2": {4, 5, 6}, 520 | "1": nil, 521 | }, 522 | map[string]*x{ 523 | "b": {A: "A", B: 128, C: true}, 524 | "a": nil, 525 | "c": {}, 526 | }, 527 | map[string]interface{}{ 528 | "1": 42, 529 | "2": "two", 530 | "3": nil, 531 | "4": (*int64)(nil), 532 | "5": x{A: "A"}, 533 | "6": &x{A: "A", B: 256, C: true}, 534 | }, 535 | } { 536 | marshalCompare(t, v, "") 537 | } 538 | } 539 | 540 | type ( 541 | mkstr string 542 | mkint int64 543 | mkvstrMarshaler string 544 | mkrstrMarshaler string 545 | mkvintMarshaler uint64 546 | mkrintMarshaler int 547 | mkvcmpMarshaler struct{} 548 | ) 549 | 550 | func (mkvstrMarshaler) MarshalText() ([]byte, error) { return []byte("MKVSTR"), nil } 551 | func (*mkrstrMarshaler) MarshalText() ([]byte, error) { return []byte("MKRSTR"), nil } 552 | func (mkvintMarshaler) MarshalText() ([]byte, error) { return []byte("MKVINT"), nil } 553 | func (*mkrintMarshaler) MarshalText() ([]byte, error) { return []byte("MKRINT"), nil } 554 | func (mkvcmpMarshaler) MarshalText() ([]byte, error) { return []byte("MKVCMP"), nil } 555 | 556 | // TestMapKeyPrecedence tests that the precedence 557 | // order of map key types is respected during marshaling. 558 | func TestMapKeyPrecedence(t *testing.T) { 559 | testdata := []interface{}{ 560 | map[mkstr]string{"K": "V"}, 561 | map[mkint]string{1: "V"}, 562 | map[mkvstrMarshaler]string{"K": "V"}, 563 | map[mkrstrMarshaler]string{"K": "V"}, 564 | map[mkvintMarshaler]string{42: "V"}, 565 | map[mkrintMarshaler]string{1: "one"}, 566 | map[mkvcmpMarshaler]string{{}: "V"}, 567 | } 568 | for _, v := range testdata { 569 | marshalCompare(t, v, "") 570 | } 571 | } 572 | 573 | // TestJSONMarshaler tests that a type implementing the 574 | // json.Marshaler interface is marshaled using the result 575 | // of its MarshalJSON method call result. 576 | // Because the types big.Int and time.Time also implements 577 | // the encoding.TextMarshaler interface, the test ensures 578 | // that MarshalJSON has priority. 579 | func TestJSONMarshaler(t *testing.T) { 580 | type x struct { 581 | T1 time.Time `json:""` 582 | T2 time.Time `json:",omitempty"` 583 | T3 *time.Time `json:""` 584 | T4 *time.Time `json:""` // nil 585 | T5 *time.Time `json:",omitempty"` // nil 586 | S1 bvjm `json:",omitempty"` 587 | S2 bvjm `json:",omitempty"` 588 | S3 bvjm `json:""` 589 | S4 *bvjm `json:""` 590 | S5 *bvjm `json:""` // nil 591 | S6 *bvjm `json:",omitempty"` // nil 592 | I1 big.Int `json:""` 593 | I2 big.Int `json:",omitempty"` 594 | I3 *big.Int `json:""` 595 | I4 *big.Int `json:""` // nil 596 | I5 *big.Int `json:",omitempty"` // nil 597 | P1 brjm `json:",omitempty"` 598 | P2 brjm `json:",omitempty"` 599 | P3 brjm `json:""` 600 | P4 *brjm `json:""` 601 | P5 *brjm `json:""` // nil 602 | P6 *brjm `json:",omitempty"` // nil 603 | 604 | // NOTE 605 | // time.Time = Non-pointer receiver of composite type. 606 | // bvjm = Non-pointer receiver of basic type. 607 | // big.Int = Pointer receiver of composite type. 608 | // brjm = Pointer receiver of basic type. 609 | } 610 | var ( 611 | now = time.Now() 612 | bval = bvjm("bval") 613 | bref = brjm("bref") 614 | xx = x{ 615 | T1: now, 616 | T3: &now, 617 | S1: "S1", 618 | S4: &bval, 619 | I1: *big.NewInt(math.MaxInt64), 620 | I3: big.NewInt(math.MaxInt64), 621 | P1: "P1", 622 | P4: &bref, 623 | } 624 | ) 625 | marshalCompare(t, xx, "non-pointer") 626 | marshalCompare(t, &xx, "pointer") 627 | } 628 | 629 | // TestTextMarshaler tests that a type implementing 630 | // the encoding.TextMarshaler interface encodes to a 631 | // quoted string of its MashalText method result. 632 | func TestTextMarshaler(t *testing.T) { 633 | type x struct { 634 | S1 net.IP `json:""` 635 | S2 net.IP `json:",omitempty"` 636 | S3 *net.IP `json:""` 637 | S4 *net.IP `json:""` // nil 638 | S5 *net.IP `json:",omitempty"` // nil 639 | I1 bvtm `json:",omitempty"` 640 | I2 bvtm `json:",omitempty"` 641 | I3 bvtm `json:""` 642 | I4 *bvtm `json:""` 643 | I5 *bvtm `json:""` // nil 644 | I6 *bvtm `json:",omitempty"` // nil 645 | F1 big.Float `json:""` 646 | F2 big.Float `json:",omitempty"` 647 | F3 *big.Float `json:""` 648 | F4 *big.Float `json:""` // nil 649 | F5 *big.Float `json:",omitempty"` // nil 650 | P1 brtm `json:",omitempty"` 651 | P2 brtm `json:",omitempty"` 652 | P3 brtm `json:""` 653 | P4 *brtm `json:""` 654 | P5 *brtm `json:""` // nil 655 | P6 *brtm `json:",omitempty"` // nil 656 | 657 | // NOTE 658 | // net.IP = Non-pointer receiver of composite type. 659 | // bvtm = Non-pointer receiver of basic type. 660 | // big.Float = Pointer receiver of composite type. 661 | // brtm = Pointer receiver of basic type. 662 | } 663 | var ( 664 | bval = bvtm(42) 665 | bref = brtm(42) 666 | xx = x{ 667 | S1: net.IP{192, 168, 0, 1}, 668 | S3: &net.IP{127, 0, 0, 1}, 669 | I1: 42, 670 | I4: &bval, 671 | F1: *big.NewFloat(math.MaxFloat64), 672 | F3: big.NewFloat(math.MaxFloat64), 673 | P1: 42, 674 | P4: &bref, 675 | } 676 | ) 677 | marshalCompare(t, xx, "non-pointer") 678 | marshalCompare(t, &xx, "pointer") 679 | } 680 | 681 | type ( 682 | bvm string 683 | brm string 684 | cvm struct{} 685 | crm struct{} 686 | ) 687 | 688 | func (m bvm) AppendJSON(dst []byte) ([]byte, error) { 689 | return append(dst, strconv.Quote(string(m))...), nil 690 | } 691 | func (m *brm) AppendJSON(dst []byte) ([]byte, error) { 692 | return append(dst, strconv.Quote(string(*m))...), nil 693 | } 694 | func (m bvm) MarshalJSON() ([]byte, error) { return []byte(strconv.Quote(string(m))), nil } 695 | func (m *brm) MarshalJSON() ([]byte, error) { return []byte(strconv.Quote(string(*m))), nil } 696 | func (cvm) AppendJSON(dst []byte) ([]byte, error) { return append(dst, `"X"`...), nil } 697 | func (cvm) MarshalJSON() ([]byte, error) { return []byte(`"X"`), nil } 698 | func (*crm) AppendJSON(dst []byte) ([]byte, error) { return append(dst, `"Y"`...), nil } 699 | func (*crm) MarshalJSON() ([]byte, error) { return []byte(`"Y"`), nil } 700 | 701 | //nolint:dupl 702 | func TestMarshaler(t *testing.T) { 703 | type x struct { 704 | S1 cvm `json:""` 705 | S2 cvm `json:",omitempty"` 706 | S3 *cvm `json:""` 707 | S4 *cvm `json:""` // nil 708 | S5 *cvm `json:",omitempty"` // nil 709 | I1 bvm `json:",omitempty"` 710 | I2 bvm `json:",omitempty"` 711 | I3 bvm `json:""` 712 | I4 *bvm `json:""` 713 | I5 *bvm `json:""` // nil 714 | I6 *bvm `json:",omitempty"` // nil 715 | F1 crm `json:""` 716 | F2 crm `json:",omitempty"` 717 | F3 *crm `json:""` 718 | F4 *crm `json:""` // nil 719 | F5 *crm `json:",omitempty"` // nil 720 | P1 brm `json:",omitempty"` 721 | P2 brm `json:",omitempty"` 722 | P3 brm `json:""` 723 | P4 *brm `json:""` 724 | P5 *brm `json:""` // nil 725 | P6 *brm `json:",omitempty"` // nil 726 | 727 | // NOTE 728 | // cvm = Non-pointer receiver of composite type. 729 | // bvm = Non-pointer receiver of basic type. 730 | // crm = Pointer receiver of composite type. 731 | // brm = Pointer receiver of basic type. 732 | } 733 | var ( 734 | bval = bvm("bval") 735 | bref = brm("bref") 736 | xx = x{ 737 | S1: cvm{}, 738 | S3: &cvm{}, 739 | I1: "I1", 740 | I4: &bval, 741 | F1: crm{}, 742 | F3: &crm{}, 743 | P1: "P1", 744 | P4: &bref, 745 | } 746 | ) 747 | marshalCompare(t, xx, "non-pointer") 748 | marshalCompare(t, &xx, "pointer") 749 | } 750 | 751 | type ( 752 | bvmctx string 753 | brmctx string 754 | cvmctx struct{} 755 | crmctx struct{} 756 | ) 757 | 758 | func (m bvmctx) AppendJSONContext(_ context.Context, dst []byte) ([]byte, error) { 759 | return append(dst, strconv.Quote(string(m))...), nil 760 | } 761 | func (m bvmctx) MarshalJSON() ([]byte, error) { 762 | return []byte(strconv.Quote(string(m))), nil 763 | } 764 | func (m *brmctx) AppendJSONContext(_ context.Context, dst []byte) ([]byte, error) { 765 | return append(dst, strconv.Quote(string(*m))...), nil 766 | } 767 | func (m *brmctx) MarshalJSON() ([]byte, error) { 768 | return []byte(strconv.Quote(string(*m))), nil 769 | } 770 | func (cvmctx) AppendJSONContext(_ context.Context, dst []byte) ([]byte, error) { 771 | return append(dst, `"X"`...), nil 772 | } 773 | func (cvmctx) MarshalJSON() ([]byte, error) { 774 | return []byte(`"X"`), nil 775 | } 776 | func (*crmctx) AppendJSONContext(_ context.Context, dst []byte) ([]byte, error) { 777 | return append(dst, `"Y"`...), nil 778 | } 779 | func (*crmctx) MarshalJSON() ([]byte, error) { 780 | return []byte(`"Y"`), nil 781 | } 782 | 783 | //nolint:dupl 784 | func TestMarshalerCtx(t *testing.T) { 785 | type x struct { 786 | S1 cvmctx `json:""` 787 | S2 cvmctx `json:",omitempty"` 788 | S3 *cvmctx `json:""` 789 | S4 *cvmctx `json:""` // nil 790 | S5 *cvmctx `json:",omitempty"` // nil 791 | I1 bvmctx `json:",omitempty"` 792 | I2 bvmctx `json:",omitempty"` 793 | I3 bvmctx `json:""` 794 | I4 *bvmctx `json:""` 795 | I5 *bvmctx `json:""` // nil 796 | I6 *bvmctx `json:",omitempty"` // nil 797 | F1 crmctx `json:""` 798 | F2 crmctx `json:",omitempty"` 799 | F3 *crmctx `json:""` 800 | F4 *crmctx `json:""` // nil 801 | F5 *crmctx `json:",omitempty"` // nil 802 | P1 brmctx `json:",omitempty"` 803 | P2 brmctx `json:",omitempty"` 804 | P3 brmctx `json:""` 805 | P4 *brmctx `json:""` 806 | P5 *brmctx `json:""` // nil 807 | P6 *brmctx `json:",omitempty"` // nil 808 | 809 | // NOTE 810 | // cvmctx = Non-pointer receiver of composite type. 811 | // bvmctx = Non-pointer receiver of basic type. 812 | // crmctx = Pointer receiver of composite type. 813 | // brmctx = Pointer receiver of basic type. 814 | } 815 | var ( 816 | bval = bvmctx("bval") 817 | bref = brmctx("bref") 818 | xx = x{ 819 | S1: cvmctx{}, 820 | S3: &cvmctx{}, 821 | I1: "I1", 822 | I4: &bval, 823 | F1: crmctx{}, 824 | F3: &crmctx{}, 825 | P1: "P1", 826 | P4: &bref, 827 | } 828 | ) 829 | marshalCompare(t, xx, "non-pointer") 830 | marshalCompare(t, &xx, "pointer") 831 | } 832 | 833 | type ( 834 | niljetim string // jettison.Marshaler 835 | nilmjctx string // jettison.MarshalerCtx 836 | niljsonm string // json.Marshaler 837 | niltextm string // encoding.TextMarshaler 838 | ) 839 | 840 | // comboMarshaler combines the json.Marshaler 841 | // and jettison.AppendMarshaler interfaces so 842 | // that tests outputs can be compared. 843 | type comboMarshaler interface { 844 | AppendMarshaler 845 | json.Marshaler 846 | } 847 | 848 | // comboMarshalerCtx combines the json.Marshaler 849 | // and jettison.AppendMarshalerCtx interfaces so 850 | // that tests outputs can be compared. 851 | type comboMarshalerCtx interface { 852 | AppendMarshalerCtx 853 | json.Marshaler 854 | } 855 | 856 | func (*niljetim) MarshalJSON() ([]byte, error) { return []byte(`"W"`), nil } 857 | func (*nilmjctx) MarshalJSON() ([]byte, error) { return []byte(`"X"`), nil } 858 | func (*niljsonm) MarshalJSON() ([]byte, error) { return []byte(`"Y"`), nil } 859 | func (*niltextm) MarshalText() ([]byte, error) { return []byte("Z"), nil } 860 | 861 | func (*niljetim) AppendJSON(dst []byte) ([]byte, error) { 862 | return append(dst, `"W"`...), nil 863 | } 864 | func (*nilmjctx) AppendJSONContext(_ context.Context, dst []byte) ([]byte, error) { 865 | return append(dst, `"X"`...), nil 866 | } 867 | 868 | type ( 869 | errvjm struct{} 870 | errrjm struct{} 871 | errvtm struct{} 872 | errrtm struct{} 873 | errvm struct{} 874 | errrm struct{} 875 | errvmctx struct{} 876 | errrmctx struct{} 877 | ) 878 | 879 | var errMarshaler = errors.New("error") 880 | 881 | func (errvjm) MarshalJSON() ([]byte, error) { return nil, errMarshaler } 882 | func (*errrjm) MarshalJSON() ([]byte, error) { return nil, errMarshaler } 883 | func (errvtm) MarshalText() ([]byte, error) { return nil, errMarshaler } 884 | func (*errrtm) MarshalText() ([]byte, error) { return nil, errMarshaler } 885 | func (errvm) AppendJSON(dst []byte) ([]byte, error) { return dst, errMarshaler } 886 | func (*errrm) AppendJSON(dst []byte) ([]byte, error) { return dst, errMarshaler } 887 | 888 | func (errvmctx) AppendJSONContext(_ context.Context, dst []byte) ([]byte, error) { 889 | return dst, errMarshaler 890 | } 891 | func (*errrmctx) AppendJSONContext(_ context.Context, dst []byte) ([]byte, error) { 892 | return dst, errMarshaler 893 | } 894 | 895 | // TestMarshalerError tests that a MarshalerError is 896 | // returned when a MarshalText, MarshalJSON, WriteJSON 897 | // or WriteJSONContext method returns an error. 898 | func TestMarshalerError(t *testing.T) { 899 | testdata := []interface{}{ 900 | errvjm{}, 901 | &errrjm{}, 902 | errvtm{}, 903 | &errrtm{}, 904 | errvm{}, 905 | &errrm{}, 906 | errvmctx{}, 907 | &errrmctx{}, 908 | } 909 | for _, v := range testdata { 910 | _, err := Marshal(v) 911 | if err != nil { 912 | me, ok := err.(*MarshalerError) 913 | if !ok { 914 | t.Fatalf("got %T, want MarshalerError", err) 915 | } 916 | typ := reflect.TypeOf(v) 917 | if me.Type != typ { 918 | t.Errorf("got %s, want %s", me.Type, typ) 919 | } 920 | if err := me.Unwrap(); err == nil { 921 | t.Error("expected non-nil error") 922 | } 923 | if me.Error() == "" { 924 | t.Error("expected non-empty error message") 925 | } 926 | } else { 927 | t.Error("got nil, want non-nil error") 928 | } 929 | } 930 | } 931 | 932 | // TestStructFieldName tests that invalid struct 933 | // field names are ignored during marshaling. 934 | func TestStructFieldName(t *testing.T) { 935 | //nolint:staticcheck 936 | type x struct { 937 | A string `json:" "` // valid, spaces 938 | B string `json:"0123"` // valid, digits 939 | C int `json:","` // invalid, comma 940 | D int8 `json:"\\"` // invalid, backslash, 941 | E int16 `json:"\""` // invalid, quotation mark 942 | F int `json:"Вилиам"` // valid, UTF-8 runes 943 | G bool `json:""` // valid, HTML-escaped chars 944 | Aβ int 945 | } 946 | marshalCompare(t, x{}, "") 947 | } 948 | 949 | // TestStructFieldOmitempty tests that the fields of 950 | // a struct with the omitempty option are not encoded 951 | // when they have the zero-value of their type. 952 | func TestStructFieldOmitempty(t *testing.T) { 953 | type x struct { 954 | A string `json:",omitempty"` 955 | B string `json:",omitempty"` 956 | C *string `json:",omitempty"` 957 | Ca *string `json:"a,omitempty"` 958 | D *string `json:",omitempty"` 959 | E bool `json:",omitempty"` 960 | F int `json:",omitempty"` 961 | F1 int8 `json:",omitempty"` 962 | F2 int16 `json:",omitempty"` 963 | F3 int32 `json:",omitempty"` 964 | F4 int64 `json:",omitempty"` 965 | G1 uint `json:",omitempty"` 966 | G2 uint8 `json:",omitempty"` 967 | G3 uint16 `json:",omitempty"` 968 | G4 uint32 `json:",omitempty"` 969 | G5 uint64 `json:",omitempty"` 970 | G6 uintptr `json:",omitempty"` 971 | H float32 `json:",omitempty"` 972 | I float64 `json:",omitempty"` 973 | J1 map[int]int `json:",omitempty"` 974 | J2 map[int]int `json:",omitempty"` 975 | J3 map[int]int `json:",omitempty"` 976 | K1 []string `json:",omitempty"` 977 | K2 []string `json:",omitempty"` 978 | L1 [0]int `json:",omitempty"` 979 | L2 [2]int `json:",omitempty"` 980 | M1 interface{} `json:",omitempty"` 981 | M2 interface{} `json:",omitempty"` 982 | } 983 | var ( 984 | s1 = "Loreum" 985 | s2 = "" 986 | xx = &x{ 987 | A: "A", 988 | B: "", 989 | C: &s1, 990 | Ca: &s2, 991 | D: nil, 992 | J2: map[int]int{}, 993 | J3: map[int]int{1: 42}, 994 | K2: []string{"K2"}, 995 | M2: (*int)(nil), 996 | } 997 | ) 998 | marshalCompare(t, xx, "") 999 | } 1000 | 1001 | // TestStructFieldOmitnil tests that the fields of a 1002 | // struct with the omitnil option are not encoded 1003 | // when they have a nil value. 1004 | func TestStructFieldOmitnil(t *testing.T) { 1005 | // nolint:staticcheck 1006 | type x struct { 1007 | Sn string `json:"sn,omitnil"` 1008 | In int `json:"in,omitnil"` 1009 | Un uint `json:"un,omitnil"` 1010 | Fn float64 `json:"fn,omitnil"` 1011 | Bn bool `json:"bn,omitnil"` 1012 | Sln []string `json:"sln,omitnil"` 1013 | Mpn map[string]interface{} `json:"mpn,omitnil"` 1014 | Stn struct{} `json:"stn,omitnil"` 1015 | Ptn *string `json:"ptn,omitnil"` 1016 | Ifn interface{} `json:"ifn,omitnil"` 1017 | } 1018 | var ( 1019 | xx = x{} 1020 | before = `{"sn":"","in":0,"un":0,"fn":0,"bn":false,"stn":{}}` 1021 | after = `{"sn":"","in":0,"un":0,"fn":0,"bn":false,"sln":[],"mpn":{},"stn":{},"ptn":"Loreum","ifn":42}` 1022 | ) 1023 | b, err := Marshal(xx) 1024 | if err != nil { 1025 | t.Fatal(err) 1026 | } 1027 | if got := string(b); got != before { 1028 | t.Errorf("before: got: %#q, want: %#q", got, before) 1029 | } 1030 | s := "Loreum" 1031 | 1032 | xx.Sln = make([]string, 0) 1033 | xx.Mpn = map[string]interface{}{} 1034 | xx.Stn = struct{}{} 1035 | xx.Ptn = &s 1036 | xx.Ifn = 42 1037 | 1038 | b, err = Marshal(xx) 1039 | if err != nil { 1040 | t.Fatal(err) 1041 | } 1042 | if got := string(b); got != after { 1043 | t.Errorf("after: got: %#q, want: %#q", got, after) 1044 | } 1045 | } 1046 | 1047 | // TestQuotedStructFields tests that the fields of 1048 | // a struct with the string option are quoted during 1049 | // marshaling if the type support it. 1050 | // 1051 | //nolint:staticcheck 1052 | func TestQuotedStructFields(t *testing.T) { 1053 | type x struct { 1054 | A1 int `json:",string"` 1055 | A2 *int `json:",string"` 1056 | A3 *int `json:",string"` 1057 | B uint `json:",string"` 1058 | C1 bool `json:",string"` 1059 | C2 *bool `json:",string"` 1060 | D float32 `json:",string"` 1061 | E string `json:",string"` 1062 | F []int `json:",string"` 1063 | G map[int]int `json:",string"` 1064 | } 1065 | var ( 1066 | i = 84 1067 | b = false 1068 | xx = &x{ 1069 | A1: -42, 1070 | A2: nil, 1071 | A3: &i, 1072 | B: 42, 1073 | C1: true, 1074 | C2: &b, 1075 | D: math.Pi, 1076 | E: "E", 1077 | F: []int{1, 2, 3}, 1078 | G: map[int]int{1: 2}, 1079 | } 1080 | ) 1081 | marshalCompare(t, xx, "") 1082 | } 1083 | 1084 | // TestBasicStructFieldTypes tests that struct 1085 | // fields of basic types can be marshaled. 1086 | func TestBasicStructFieldTypes(t *testing.T) { 1087 | type x struct { 1088 | A string `json:"a"` 1089 | B1 int `json:"b1"` 1090 | B2 int8 `json:"b2"` 1091 | B3 int16 `json:"b3"` 1092 | B4 int32 `json:"b4"` 1093 | B5 int64 `json:"b5"` 1094 | C1 uint `json:"c1"` 1095 | C2 uint8 `json:"c2"` 1096 | C3 uint16 `json:"c3"` 1097 | C4 uint32 `json:"c4"` 1098 | C5 uint64 `json:"c5"` 1099 | D1 bool `json:"d1"` 1100 | D2 bool `json:"d2"` 1101 | E float32 `json:"e"` 1102 | F float64 `json:"f"` 1103 | G string `json:"-"` // ignored 1104 | H string `json:"-,"` // use "-" as key 1105 | i string 1106 | } 1107 | xx := &x{ 1108 | A: "A", 1109 | B1: -42, 1110 | B2: math.MinInt8, 1111 | B3: math.MinInt16, 1112 | B4: math.MinInt32, 1113 | B5: math.MinInt64, 1114 | C1: 42, 1115 | C2: math.MaxUint8, 1116 | C3: math.MaxUint16, 1117 | C4: math.MaxUint32, 1118 | C5: math.MaxUint64, 1119 | D1: true, 1120 | D2: false, 1121 | E: 3.14169, 1122 | F: math.MaxFloat64, 1123 | G: "ignored", 1124 | H: "not-ignored", 1125 | i: "unexported", 1126 | } 1127 | marshalCompare(t, xx, "non-pointer") 1128 | marshalCompare(t, &xx, "pointer") 1129 | } 1130 | 1131 | // TestBasicStructFieldPointerTypes tests 1132 | // that nil and non-nil struct field pointers 1133 | // of basic types can be marshaled. 1134 | func TestBasicStructFieldPointerTypes(t *testing.T) { 1135 | type x struct { 1136 | A *string `json:"a"` 1137 | B *int `json:"b"` 1138 | C *uint64 `json:"c"` 1139 | D *bool `json:"d"` 1140 | E *float32 `json:"e"` 1141 | F *float64 `json:"f"` 1142 | } 1143 | var ( 1144 | a = "a" 1145 | b = 42 1146 | d = true 1147 | f = math.MaxFloat64 1148 | xx = x{A: &a, B: &b, C: nil, D: &d, E: nil, F: &f} 1149 | ) 1150 | marshalCompare(t, xx, "non-pointer") 1151 | marshalCompare(t, &xx, "pointer") 1152 | } 1153 | 1154 | // TestCompositeStructFieldTypes tests that struct 1155 | // fields of composite types, such as struct, slice, 1156 | // array and map can be marshaled. 1157 | func TestCompositeStructFieldTypes(t *testing.T) { 1158 | type y struct { 1159 | X string `json:"x"` 1160 | } 1161 | type x struct { 1162 | A y `json:"a"` 1163 | B1 *y 1164 | B2 *y 1165 | b3 *y 1166 | c1 []string 1167 | C2 []string 1168 | D []int 1169 | E []bool 1170 | F []float32 1171 | G []*uint 1172 | H [3]string 1173 | I [1]int 1174 | J [0]bool 1175 | K1 []byte 1176 | K2 []byte 1177 | L []*int 1178 | M1 []y 1179 | M2 *[]y 1180 | N1 []*y 1181 | N2 []*y 1182 | O1 [3]*int 1183 | O2 *[3]*bool 1184 | P [3]*y 1185 | Q [][]int 1186 | R [2][2]string 1187 | S1 map[int]string 1188 | S2 map[int]string 1189 | S3 map[int]string 1190 | S4 map[string]interface{} 1191 | T1 *map[string]int 1192 | T2 *map[string]int 1193 | T3 *map[string]int 1194 | U1 interface{} 1195 | U2 interface{} 1196 | U3 interface{} 1197 | U4 interface{} 1198 | U5 interface{} 1199 | U6 interface{} 1200 | u7 interface{} 1201 | } 1202 | k := make([]byte, 32) 1203 | if _, err := rand.Read(k); err != nil { 1204 | t.Error(err) 1205 | } 1206 | var ( 1207 | l1 = 0 1208 | l2 = 42 1209 | m1 = y{X: "X"} 1210 | m2 = y{} 1211 | i0 = 42 1212 | i1 = &i0 1213 | i2 = &i1 1214 | i3 = &i2 1215 | xx = x{ 1216 | A: y{X: "X"}, 1217 | B1: nil, 1218 | B2: &y{X: "Ipsum"}, 1219 | b3: nil, 1220 | c1: nil, 1221 | C2: []string{"one", "two", "three"}, 1222 | D: []int{1, 2, 3}, 1223 | E: []bool{}, 1224 | H: [3]string{"alpha", "beta", "gamma"}, 1225 | I: [1]int{42}, 1226 | K1: k, 1227 | K2: []byte(nil), 1228 | L: []*int{&l1, &l2, nil}, 1229 | M1: []y{m1, m2}, 1230 | N1: []*y{&m1, &m2, nil}, 1231 | N2: []*y{}, 1232 | O1: [3]*int{&l1, &l2, nil}, 1233 | P: [3]*y{&m1, &m2, nil}, 1234 | Q: [][]int{{1, 2}, {3, 4}}, 1235 | R: [2][2]string{{"a", "b"}, {"c", "d"}}, 1236 | S1: nil, 1237 | S3: map[int]string{1: "x", 2: "y", 3: "z"}, 1238 | S4: map[string]interface{}{"a": 1, "b": "2"}, 1239 | T3: &map[string]int{"x": 1, "y": 2, "z": 3}, 1240 | U1: "U1", 1241 | U2: &l2, 1242 | U3: nil, 1243 | U4: false, 1244 | U5: (*int)(nil), // typed nil 1245 | U6: i3, // chain of pointers 1246 | u7: nil, 1247 | } 1248 | ) 1249 | marshalCompare(t, xx, "non-pointer") 1250 | marshalCompare(t, &xx, "pointer") 1251 | } 1252 | 1253 | // TestEmbeddedTypes tests that composite and basic 1254 | // embedded struct fields types are encoded whether 1255 | // they are exported. 1256 | func TestEmbeddedTypes(t *testing.T) { 1257 | type ( 1258 | P1 int 1259 | P2 string 1260 | P3 bool 1261 | p4 uint32 1262 | C1 map[string]int 1263 | C2 [3]string 1264 | C3 []int 1265 | c4 []bool 1266 | ) 1267 | type x struct { 1268 | P1 1269 | P2 1270 | P3 1271 | p4 1272 | C1 1273 | C2 1274 | C3 1275 | c4 `json:"c4"` 1276 | } 1277 | xx := &x{ 1278 | P1: P1(42), 1279 | P2: P2("P2"), 1280 | P3: P3(true), 1281 | p4: p4(math.MaxUint32), 1282 | C1: C1{"A": 1, "B": 2}, 1283 | C2: C2{"A", "B", "C"}, 1284 | C3: C3{1, 2, 3}, 1285 | c4: c4{true, false}, 1286 | } 1287 | marshalCompare(t, xx, "") 1288 | } 1289 | 1290 | // TestRecursiveType tests the marshaling of 1291 | // recursive types. 1292 | func TestRecursiveType(t *testing.T) { 1293 | type x struct { 1294 | A string `json:"a"` 1295 | X *x `json:"x"` 1296 | } 1297 | xx := &x{ 1298 | A: "A1", 1299 | X: &x{A: "A2"}, 1300 | } 1301 | marshalCompare(t, xx, "") 1302 | } 1303 | 1304 | // TestTaggedFieldDominates tests that a struct 1305 | // field with a tag dominates untagged fields. 1306 | func TestTaggedFieldDominates(t *testing.T) { 1307 | type ( 1308 | A struct{ S string } 1309 | D struct { 1310 | XXX string `json:"S"` 1311 | } 1312 | Y struct { 1313 | A 1314 | D 1315 | } 1316 | ) 1317 | y := Y{ 1318 | A{"A"}, 1319 | D{"D"}, 1320 | } 1321 | marshalCompare(t, y, "") 1322 | } 1323 | 1324 | // TestDuplicatedFieldDisappears tests that 1325 | // duplicate struct field at the same level 1326 | // of embedding are ignored. 1327 | func TestDuplicatedFieldDisappears(t *testing.T) { 1328 | type ( 1329 | A struct{ S string } 1330 | C struct{ S string } 1331 | D struct { 1332 | XXX string `json:"S"` 1333 | } 1334 | Y struct { 1335 | A 1336 | D 1337 | } 1338 | Z struct { 1339 | A 1340 | C 1341 | Y 1342 | } 1343 | ) 1344 | z := Z{A{"A"}, C{"C"}, Y{A{"S"}, D{"D"}}} 1345 | 1346 | marshalCompare(t, z, "") 1347 | } 1348 | 1349 | // TestEmbeddedStructs tests that named and unnamed 1350 | // embedded structs fields can be marshaled. 1351 | func TestEmbeddedStructs(t *testing.T) { 1352 | type ( 1353 | r struct { 1354 | J string `json:"j"` 1355 | } 1356 | v struct { 1357 | H bool `json:"h,omitempty"` 1358 | I string `json:"i"` 1359 | } 1360 | y struct { 1361 | D int8 `json:"d"` 1362 | E uint8 `json:"e,omitempty"` 1363 | r 1364 | v 1365 | } 1366 | z struct { 1367 | F int16 `json:"f,omitempty"` 1368 | G uint16 `json:"g"` 1369 | y 1370 | v 1371 | } 1372 | // According to the Go rules for embedded fields, 1373 | // y.r.J should be encoded while z.y.r.J is not, 1374 | // because is one-level up. 1375 | // However, y.v.H and z.v.H are present at the same 1376 | // level, and therefore are both hidden. 1377 | x1 struct { 1378 | A string `json:"a,omitempty"` 1379 | y 1380 | B string `json:"b"` 1381 | v `json:"v"` 1382 | C string `json:"c,omitempty"` 1383 | z `json:",omitempty"` 1384 | *x1 1385 | } 1386 | // x2 is a variant of the x1 type without 1387 | // the omitempty option on the first field. 1388 | x2 struct { 1389 | A int16 `json:"a"` 1390 | v `json:"v"` 1391 | } 1392 | ) 1393 | xx1 := &x1{ 1394 | A: "A", 1395 | y: y{ 1396 | D: math.MinInt8, 1397 | r: r{J: "J"}, 1398 | v: v{H: false}, 1399 | }, 1400 | z: z{ 1401 | G: math.MaxUint16, 1402 | y: y{D: 21, r: r{J: "J"}}, 1403 | v: v{H: true}, 1404 | }, 1405 | x1: &x1{ 1406 | A: "A", 1407 | }, 1408 | } 1409 | xx2 := &x2{A: 42, v: v{I: "I"}} 1410 | 1411 | marshalCompare(t, xx1, "") 1412 | marshalCompare(t, xx2, "") 1413 | } 1414 | 1415 | // TestAnonymousFields tests the marshaling of 1416 | // advanced cases for anonymous struct fields. 1417 | // Adapted from the encoding/json testsuite. 1418 | func TestAnonymousFields(t *testing.T) { 1419 | testdata := []struct { 1420 | label string 1421 | input func() []interface{} 1422 | }{{ 1423 | // Both S1 and S2 have a field named X. 1424 | // From the perspective of S, it is 1425 | // ambiguous which one X refers to. 1426 | // This should not encode either field. 1427 | label: "AmbiguousField", 1428 | input: func() []interface{} { 1429 | type ( 1430 | S1 struct{ x, X int } 1431 | S2 struct{ x, X int } 1432 | S struct { 1433 | S1 1434 | S2 1435 | } 1436 | ) 1437 | return []interface{}{ 1438 | S{S1{1, 2}, S2{3, 4}}, 1439 | &S{S1{5, 6}, S2{7, 8}}, 1440 | } 1441 | }, 1442 | }, { 1443 | // Both S1 and S2 have a field named X, but 1444 | // since S has an X field as well, it takes 1445 | // precedence over S1.X and S2.X. 1446 | label: "DominantField", 1447 | input: func() []interface{} { 1448 | type ( 1449 | S1 struct{ x, X int } 1450 | S2 struct{ x, X int } 1451 | S struct { 1452 | S1 1453 | S2 1454 | x, X int 1455 | } 1456 | ) 1457 | return []interface{}{ 1458 | S{S1{1, 2}, S2{3, 4}, 5, 6}, 1459 | &S{S1{6, 5}, S2{4, 3}, 2, 1}, 1460 | } 1461 | }, 1462 | }, { 1463 | // Unexported embedded field of non-struct type 1464 | // should not be serialized. 1465 | label: "UnexportedEmbeddedInt", 1466 | input: func() []interface{} { 1467 | type ( 1468 | i int 1469 | S struct{ i } 1470 | ) 1471 | return []interface{}{S{5}, &S{6}} 1472 | }, 1473 | }, { 1474 | // Exported embedded field of non-struct type 1475 | // should be serialized. 1476 | label: "ExportedEmbeddedInt", 1477 | input: func() []interface{} { 1478 | type ( 1479 | I int 1480 | S struct{ I } 1481 | ) 1482 | return []interface{}{S{5}, &S{6}} 1483 | }, 1484 | }, { 1485 | // Unexported embedded field of pointer to 1486 | // non-struct type should not be serialized. 1487 | label: "UnexportedEmbeddedIntPointer", 1488 | input: func() []interface{} { 1489 | type ( 1490 | i int 1491 | S struct{ *i } 1492 | ) 1493 | s := S{new(i)} 1494 | *s.i = 5 1495 | return []interface{}{s, &s} 1496 | }, 1497 | }, { 1498 | // Exported embedded field of pointer to 1499 | // non-struct type should be serialized. 1500 | label: "ExportedEmbeddedIntPointer", 1501 | input: func() []interface{} { 1502 | type ( 1503 | I int 1504 | S struct{ *I } 1505 | ) 1506 | s := S{new(I)} 1507 | *s.I = 5 1508 | return []interface{}{s, &s} 1509 | }, 1510 | }, { 1511 | // Exported embedded field of nil pointer 1512 | // to non-struct type should be serialized. 1513 | label: "ExportedEmbeddedNilIntPointer", 1514 | input: func() []interface{} { 1515 | type ( 1516 | I int 1517 | S struct{ *I } 1518 | ) 1519 | s := S{new(I)} 1520 | s.I = nil 1521 | return []interface{}{s, &s} 1522 | }, 1523 | }, { 1524 | // Exported embedded field of nil pointer to 1525 | // non-struct type should not be serialized 1526 | // if it has the omitempty option. 1527 | label: "ExportedEmbeddedNilIntPointerOmitempty", 1528 | input: func() []interface{} { 1529 | type ( 1530 | I int 1531 | S struct { 1532 | *I `json:",omitempty"` 1533 | } 1534 | ) 1535 | s := S{new(I)} 1536 | s.I = nil 1537 | return []interface{}{s, &s} 1538 | }, 1539 | }, { 1540 | // Exported embedded field of pointer to 1541 | // struct type should be serialized. 1542 | label: "ExportedEmbeddedStructPointer", 1543 | input: func() []interface{} { 1544 | type ( 1545 | S struct{ X string } 1546 | T struct{ *S } 1547 | ) 1548 | t := T{S: &S{ 1549 | X: "X", 1550 | }} 1551 | return []interface{}{t, &t} 1552 | }, 1553 | }, { 1554 | // Exported fields of embedded structs should 1555 | // have their exported fields be serialized 1556 | // regardless of whether the struct types 1557 | // themselves are exported. 1558 | label: "EmbeddedStructNonPointer", 1559 | input: func() []interface{} { 1560 | type ( 1561 | s1 struct{ x, X int } 1562 | S2 struct{ y, Y int } 1563 | S struct { 1564 | s1 1565 | S2 1566 | } 1567 | ) 1568 | return []interface{}{ 1569 | S{s1{1, 2}, S2{3, 4}}, 1570 | &S{s1{5, 6}, S2{7, 8}}, 1571 | } 1572 | }, 1573 | }, { 1574 | // Exported fields of pointers to embedded 1575 | // structs should have their exported fields 1576 | // be serialized regardless of whether the 1577 | // struct types themselves are exported. 1578 | label: "EmbeddedStructPointer", 1579 | input: func() []interface{} { 1580 | type ( 1581 | s1 struct{ x, X int } 1582 | S2 struct{ y, Y int } 1583 | S struct { 1584 | *s1 1585 | *S2 1586 | } 1587 | ) 1588 | return []interface{}{ 1589 | S{&s1{1, 2}, &S2{3, 4}}, 1590 | &S{&s1{5, 6}, &S2{7, 8}}, 1591 | } 1592 | }, 1593 | }, { 1594 | // Exported fields on embedded unexported 1595 | // structs at multiple levels of nesting 1596 | // should still be serialized. 1597 | label: "NestedStructAndInts", 1598 | input: func() []interface{} { 1599 | type ( 1600 | I1 int 1601 | I2 int 1602 | i int 1603 | s2 struct { 1604 | I2 1605 | i 1606 | } 1607 | s1 struct { 1608 | I1 1609 | i 1610 | s2 1611 | } 1612 | S struct { 1613 | s1 1614 | i 1615 | } 1616 | ) 1617 | return []interface{}{ 1618 | S{s1{1, 2, s2{3, 4}}, 5}, 1619 | &S{s1{5, 4, s2{3, 2}}, 1}, 1620 | } 1621 | }, 1622 | }, { 1623 | // If an anonymous struct pointer field is nil, 1624 | // we should ignore the embedded fields behind it. 1625 | // Not properly doing so may result in the wrong 1626 | // output or a panic. 1627 | label: "EmbeddedFieldBehindNilPointer", 1628 | input: func() []interface{} { 1629 | type ( 1630 | S2 struct{ Field string } 1631 | S struct{ *S2 } 1632 | ) 1633 | return []interface{}{S{}, &S{}} 1634 | }, 1635 | }, { 1636 | // A field behind a chain of pointer and 1637 | // non-pointer embedded fields should be 1638 | // accessible and serialized. 1639 | label: "BasicEmbeddedFieldChain", 1640 | input: func() []interface{} { 1641 | type ( 1642 | A struct { 1643 | X1 string 1644 | X2 *string 1645 | } 1646 | B struct{ *A } 1647 | C struct{ B } 1648 | D struct{ *C } 1649 | E struct{ D } 1650 | F struct{ *E } 1651 | ) 1652 | s := "Loreum" 1653 | f := F{E: &E{D: D{C: &C{B: B{A: &A{X1: "X1", X2: &s}}}}}} 1654 | return []interface{}{f, &f} 1655 | }, 1656 | }, { 1657 | // Variant of the test above, with embedded 1658 | // fields of type struct that contain one or 1659 | // more fields themselves. 1660 | label: "ComplexEmbeddedFieldChain", 1661 | input: func() []interface{} { 1662 | type ( 1663 | A struct { 1664 | X1 string `json:",omitempty"` 1665 | X2 string 1666 | } 1667 | B struct { 1668 | Z3 *bool 1669 | A 1670 | } 1671 | C struct{ B } 1672 | D struct { 1673 | *C 1674 | Z2 int 1675 | } 1676 | E struct{ *D } 1677 | F struct { 1678 | Z1 string `json:",omitempty"` 1679 | *E 1680 | } 1681 | ) 1682 | f := F{Z1: "Z1", E: &E{D: &D{C: &C{B: B{A: A{X2: "X2"}, Z3: new(bool)}}, Z2: 1}}} 1683 | return []interface{}{f, &f} 1684 | }, 1685 | }} 1686 | for i := range testdata { 1687 | e := testdata[i] 1688 | t.Run(e.label, func(t *testing.T) { 1689 | for i, input := range e.input() { 1690 | input := input 1691 | var label string 1692 | if i == 0 { 1693 | label = "non-pointer" 1694 | } else { 1695 | label = "pointer" 1696 | } 1697 | t.Run(label, func(t *testing.T) { 1698 | marshalCompare(t, input, label) 1699 | }) 1700 | } 1701 | }) 1702 | } 1703 | } 1704 | 1705 | func TestBytesEscaping(t *testing.T) { 1706 | testdata := []struct { 1707 | in, out string 1708 | }{ 1709 | {"\x00", `"\u0000"`}, 1710 | {"\x01", `"\u0001"`}, 1711 | {"\x02", `"\u0002"`}, 1712 | {"\x03", `"\u0003"`}, 1713 | {"\x04", `"\u0004"`}, 1714 | {"\x05", `"\u0005"`}, 1715 | {"\x06", `"\u0006"`}, 1716 | {"\x07", `"\u0007"`}, 1717 | {"\x08", `"\u0008"`}, 1718 | {"\x09", `"\t"`}, 1719 | {"\x0a", `"\n"`}, 1720 | {"\x0b", `"\u000b"`}, 1721 | {"\x0c", `"\u000c"`}, 1722 | {"\x0d", `"\r"`}, 1723 | {"\x0e", `"\u000e"`}, 1724 | {"\x0f", `"\u000f"`}, 1725 | {"\x10", `"\u0010"`}, 1726 | {"\x11", `"\u0011"`}, 1727 | {"\x12", `"\u0012"`}, 1728 | {"\x13", `"\u0013"`}, 1729 | {"\x14", `"\u0014"`}, 1730 | {"\x15", `"\u0015"`}, 1731 | {"\x16", `"\u0016"`}, 1732 | {"\x17", `"\u0017"`}, 1733 | {"\x18", `"\u0018"`}, 1734 | {"\x19", `"\u0019"`}, 1735 | {"\x1a", `"\u001a"`}, 1736 | {"\x1b", `"\u001b"`}, 1737 | {"\x1c", `"\u001c"`}, 1738 | {"\x1d", `"\u001d"`}, 1739 | {"\x1e", `"\u001e"`}, 1740 | {"\x1f", `"\u001f"`}, 1741 | } 1742 | for _, tt := range testdata { 1743 | b, err := Marshal(tt.in) 1744 | if err != nil { 1745 | t.Error(err) 1746 | } 1747 | if s := string(b); s != tt.out { 1748 | t.Errorf("got %#q, want %#q", s, tt.out) 1749 | } 1750 | } 1751 | } 1752 | 1753 | // TestStringEscaping tests that control and reserved 1754 | // JSON characters are properly escaped when a string 1755 | // is marshaled. 1756 | func TestStringEscaping(t *testing.T) { 1757 | b := []byte{ 1758 | 'A', 1, 2, 3, 1759 | '"', '\\', '/', '\b', '\f', '\n', '\r', '\t', 1760 | 0xC7, 0xA3, 0xE2, 0x80, 0xA8, 0xE2, 0x80, 0xA9, 1761 | } 1762 | testdata := []struct { 1763 | b []byte 1764 | s string 1765 | opt Option 1766 | cmp bool 1767 | }{ 1768 | {b, `"A\u0001\u0002\u0003\"\\/\u0008\u000c\n\r\tǣ\u2028\u2029"`, nil, true}, 1769 | {b, `"` + string(b) + `"`, NoStringEscaping(), false}, 1770 | } 1771 | for _, tt := range testdata { 1772 | b, err := MarshalOpts(string(tt.b), tt.opt) 1773 | if err != nil { 1774 | t.Error(err) 1775 | } 1776 | if s := string(b); s != tt.s { 1777 | t.Errorf("got %#q, want %#q", s, tt.s) 1778 | } 1779 | if tt.cmp { 1780 | bs, err := json.Marshal(string(tt.b)) 1781 | if err != nil { 1782 | t.Error(err) 1783 | } 1784 | if !bytes.Equal(bs, b) { 1785 | t.Logf("standard: %s", bs) 1786 | t.Logf("jettison: %s", b) 1787 | t.Errorf("expected equal outputs") 1788 | } 1789 | } 1790 | } 1791 | } 1792 | 1793 | // TestStringHTMLEscaping tests that HTML characters 1794 | // are properly escaped when a string is marshaled. 1795 | func TestStringHTMLEscaping(t *testing.T) { 1796 | htmlChars := []byte{'<', '>', '&'} 1797 | testdata := []struct { 1798 | b []byte 1799 | s string 1800 | opts []Option 1801 | }{ 1802 | {htmlChars, `"\u003c\u003e\u0026"`, nil}, 1803 | {htmlChars, `"<>&"`, []Option{NoHTMLEscaping()}}, 1804 | 1805 | // NoHTMLEscaping is ignored when NoStringEscaping 1806 | // is set, because it's part of the escaping options. 1807 | {htmlChars, `"<>&"`, []Option{NoStringEscaping()}}, 1808 | {htmlChars, `"<>&"`, []Option{NoStringEscaping(), NoHTMLEscaping()}}, 1809 | } 1810 | for _, tt := range testdata { 1811 | b, err := MarshalOpts(string(tt.b), tt.opts...) 1812 | if err != nil { 1813 | t.Error(err) 1814 | } 1815 | if s := string(b); s != tt.s { 1816 | t.Errorf("got %#q, want %#q", s, tt.s) 1817 | } 1818 | } 1819 | } 1820 | 1821 | // TestStringUTF8Coercion tests that invalid bytes 1822 | // are replaced by the Unicode replacement rune when 1823 | // a string is marshaled. 1824 | func TestStringUTF8Coercion(t *testing.T) { 1825 | utf8Seq := string([]byte{'H', 'e', 'l', 'l', 'o', ',', ' ', 0xff, 0xfe, 0xff}) 1826 | testdata := []struct { 1827 | b string 1828 | s string 1829 | opt Option 1830 | }{ 1831 | {utf8Seq, `"Hello, \ufffd\ufffd\ufffd"`, nil}, 1832 | {utf8Seq, `"` + utf8Seq + `"`, NoUTF8Coercion()}, 1833 | } 1834 | for _, tt := range testdata { 1835 | b, err := MarshalOpts(tt.b, tt.opt) 1836 | if err != nil { 1837 | t.Error(err) 1838 | } 1839 | if s := string(b); s != tt.s { 1840 | t.Errorf("got %#q, want %#q", s, tt.s) 1841 | } 1842 | } 1843 | } 1844 | 1845 | func TestMarshalFloat(t *testing.T) { 1846 | // Taken from encoding/json. 1847 | t.Parallel() 1848 | 1849 | nf := 0 1850 | mc := regexp.MustCompile 1851 | re := []*regexp.Regexp{ 1852 | mc(`p`), 1853 | mc(`^\+`), 1854 | mc(`^-?0[^.]`), 1855 | mc(`^-?\.`), 1856 | mc(`\.(e|$)`), 1857 | mc(`\.[0-9]+0(e|$)`), 1858 | mc(`^-?(0|[0-9]{2,})\..*e`), 1859 | mc(`e[0-9]`), 1860 | mc(`e[+-]0`), 1861 | mc(`e-[1-6]$`), 1862 | mc(`e+(.|1.|20)$`), 1863 | mc(`^-?0\.0000000`), 1864 | mc(`^-?[0-9]{22}`), 1865 | mc(`[1-9][0-9]{16}[1-9]`), 1866 | mc(`[1-9][0-9.]{17}[1-9]`), 1867 | mc(`[1-9][0-9]{8}[1-9]`), 1868 | mc(`[1-9][0-9.]{9}[1-9]`), 1869 | } 1870 | fn := func(f float64, bits int) { 1871 | vf := interface{}(f) 1872 | if bits == 32 { 1873 | f = float64(float32(f)) // round 1874 | vf = float32(f) 1875 | } 1876 | bout, err := Marshal(vf) 1877 | if err != nil { 1878 | t.Errorf("Encode(%T(%g)): %v", vf, vf, err) 1879 | nf++ 1880 | return 1881 | } 1882 | out := string(bout) 1883 | 1884 | // Result must convert back to the same float. 1885 | g, err := strconv.ParseFloat(out, bits) 1886 | if err != nil { 1887 | t.Errorf("%T(%g) = %q, cannot parse back: %v", vf, vf, out, err) 1888 | nf++ 1889 | return 1890 | } 1891 | if f != g || fmt.Sprint(f) != fmt.Sprint(g) { // fmt.Sprint handles ±0 1892 | t.Errorf("%T(%g) = %q (is %g, not %g)", vf, vf, out, float32(g), vf) 1893 | nf++ 1894 | return 1895 | } 1896 | bad := re 1897 | if bits == 64 { 1898 | // Last two regexps are for 32-bits values only. 1899 | bad = bad[:len(bad)-2] 1900 | } 1901 | for _, re := range bad { 1902 | if re.MatchString(out) { 1903 | t.Errorf("%T(%g) = %q, must not match /%s/", vf, vf, out, re) 1904 | nf++ 1905 | return 1906 | } 1907 | } 1908 | } 1909 | fn(0, 64) 1910 | fn(math.Copysign(0, -1), 64) 1911 | fn(0, 32) 1912 | fn(math.Copysign(0, -1), 32) 1913 | 1914 | var ( 1915 | bigger = math.Inf(+1) 1916 | smaller = math.Inf(-1) 1917 | digits = "1.2345678901234567890123" 1918 | ) 1919 | for i := len(digits); i >= 2; i-- { 1920 | if testing.Short() && i < len(digits)-4 { 1921 | break 1922 | } 1923 | for exp := -30; exp <= 30; exp++ { 1924 | for _, sign := range "+-" { 1925 | for bits := 32; bits <= 64; bits += 32 { 1926 | s := fmt.Sprintf("%c%se%d", sign, digits[:i], exp) 1927 | f, err := strconv.ParseFloat(s, bits) 1928 | if err != nil { 1929 | t.Fatal(err) 1930 | } 1931 | next := math.Nextafter 1932 | if bits == 32 { 1933 | next = func(g, h float64) float64 { 1934 | return float64(math.Nextafter32(float32(g), float32(h))) 1935 | } 1936 | } 1937 | fn(f, bits) 1938 | fn(next(f, bigger), bits) 1939 | fn(next(f, smaller), bits) 1940 | 1941 | if nf > 50 { 1942 | t.Fatalf("too many fails, stopping tests early") 1943 | } 1944 | } 1945 | } 1946 | } 1947 | } 1948 | } 1949 | 1950 | type ( 1951 | jm int 1952 | jmp int 1953 | ) 1954 | 1955 | func (m jm) MarshalJSON() ([]byte, error) { 1956 | if m == 0 { 1957 | return []byte("null"), nil 1958 | } 1959 | return []byte(strconv.Itoa(int(m))), nil 1960 | } 1961 | 1962 | func (m *jmp) MarshalJSON() ([]byte, error) { 1963 | if m == nil || *m == 0 { 1964 | return []byte("null"), nil 1965 | } 1966 | return []byte(strconv.Itoa(int(*m))), nil 1967 | } 1968 | 1969 | func TestIssue5(t *testing.T) { 1970 | type X struct { 1971 | JMA jm `json:"jma,omitnil"` 1972 | JMB jm `json:"jmb,omitnil"` 1973 | JMC *jm `json:"jmc,omitnil"` 1974 | JMD *jm `json:"jmd,omitnil"` 1975 | JME jm `json:"jme"` 1976 | JMF *jm `json:"jmf"` 1977 | JMG *jm `json:"jmg,omitnil"` 1978 | JMPA jmp `json:"jmpa,omitnil"` 1979 | JMPB jmp `json:"jmpb,omitnil"` 1980 | JMPC *jmp `json:"jmpc,omitnil"` 1981 | JMPD *jmp `json:"jmpd,omitnil"` 1982 | JMPE *jmp `json:"jmpe"` 1983 | JMPF *jmp `json:"jmpf"` 1984 | JMPG jmp `json:"jmpg"` 1985 | JMPH *jmp `json:"jmph,omitnil"` 1986 | } 1987 | var ( 1988 | jmc = jm(2) 1989 | jmd = jm(0) 1990 | jmpc = jmp(2) 1991 | jmpd = jmp(0) 1992 | ) 1993 | x := X{ 1994 | JMA: jm(4), 1995 | JMB: jm(0), 1996 | JMC: &jmc, 1997 | JMD: &jmd, 1998 | JME: jm(0), 1999 | JMF: &jmd, 2000 | JMG: nil, 2001 | JMPA: jmp(4), 2002 | // note: the JMPB field implementation of MarshalJSON 2003 | // has a pointer-receiver, but the field itself is not 2004 | // a pointer, therefore the method is not invoked and 2005 | // the omitnil option does not apply. 2006 | JMPB: jmp(0), 2007 | JMPC: &jmpc, 2008 | JMPD: &jmpd, 2009 | JMPE: nil, 2010 | JMPF: &jmpd, 2011 | JMPG: jmp(0), // same as JMPB, 2012 | JMPH: nil, 2013 | } 2014 | b, err := Marshal(x) 2015 | if err != nil { 2016 | t.Error(err) 2017 | } 2018 | want := []byte(`{"jma":4,"jmc":2,"jme":null,"jmf":null,"jmpa":4,"jmpb":0,"jmpc":2,"jmpe":null,"jmpf":null,"jmpg":0}`) 2019 | if bytes.Compare(b, want) != 0 { 2020 | t.Errorf("got %s, want %s,", string(b), string(want)) 2021 | } 2022 | } 2023 | --------------------------------------------------------------------------------