pax_global_header00006660000000000000000000000064143032166320014512gustar00rootroot0000000000000052 comment=f99d44077c2d4913aed759cb79451bf55fb21387 go-jsonstream-3.0.0/000077500000000000000000000000001430321663200143025ustar00rootroot00000000000000go-jsonstream-3.0.0/.circleci/000077500000000000000000000000001430321663200161355ustar00rootroot00000000000000go-jsonstream-3.0.0/.circleci/config.yml000066400000000000000000000053751430321663200201370ustar00rootroot00000000000000version: 2.1 workflows: workflow: jobs: - go-test: name: Go 1.19 docker-image: cimg/go:1.19 run-lint: true # golangci-lint doesn't yet work in Go 1.18 - go-test: name: Go 1.18 docker-image: cimg/go:1.18 - benchmarks jobs: go-test: parameters: docker-image: type: string run-lint: type: boolean default: false with-coverage: type: boolean default: false docker: - image: <> environment: CIRCLE_TEST_REPORTS: /tmp/circle-reports CIRCLE_ARTIFACTS: /tmp/circle-artifacts steps: - checkout - run: name: install go-junit-report command: go install github.com/jstemmer/go-junit-report/v2@v2.0.0 - run: name: build (default implementation) command: make build - run: name: build (easyjson implementation) command: make build-easyjson - when: condition: <> steps: - run: make lint - run: name: run tests (default implementation) command: | mkdir -p $CIRCLE_TEST_REPORTS mkdir -p $CIRCLE_ARTIFACTS make test | tee $CIRCLE_ARTIFACTS/report.txt - run: name: run tests (easyjson implementation) command: | make test-easyjson | tee -a $CIRCLE_ARTIFACTS/report.txt - run: name: Process test results command: go-junit-report < $CIRCLE_ARTIFACTS/report.txt > $CIRCLE_TEST_REPORTS/junit.xml when: always - when: condition: <> steps: - run: name: Verify test coverage command: make test-coverage - run: name: Store coverage results command: cp build/coverage* /tmp/circle-artifacts when: always - store_test_results: path: /tmp/circle-reports - store_artifacts: path: /tmp/circle-artifacts benchmarks: docker: - image: cimg/go:1.19 environment: CIRCLE_ARTIFACTS: /tmp/circle-artifacts steps: - checkout - run: go build ./... - run: name: run benchmarks (default implementation) command: | mkdir -p $CIRCLE_ARTIFACTS make benchmarks | tee $CIRCLE_ARTIFACTS/benchmarks.txt - run: name: run benchmarks (easyjson implementation) command: make benchmarks-easyjson | tee $CIRCLE_ARTIFACTS/benchmarks-easyjson.txt - store_artifacts: path: /tmp/circle-artifacts go-jsonstream-3.0.0/.gitignore000066400000000000000000000000241430321663200162660ustar00rootroot00000000000000bin/ build/ .vscode go-jsonstream-3.0.0/.golangci.yml000066400000000000000000000014461430321663200166730ustar00rootroot00000000000000run: deadline: 120s tests: false skip-dirs: commontest linters: enable: - bodyclose - deadcode - depguard - dupl - errcheck - goconst - gochecknoglobals - gochecknoinits - goconst - gocritic - gocyclo - godox - gofmt - goimports - gosec - gosimple - govet - ineffassign - lll - megacheck - misspell - nakedret - nolintlint - prealloc - revive - staticcheck - stylecheck - typecheck - unconvert - unparam - unused - varcheck - whitespace fast: false linters-settings: gofmt: simplify: false goimports: local-prefixes: gopkg.in/launchdarkly,github.com/launchdarkly issues: exclude-use-default: false max-same-issues: 1000 max-per-linter: 1000 go-jsonstream-3.0.0/.ldrelease/000077500000000000000000000000001430321663200163205ustar00rootroot00000000000000go-jsonstream-3.0.0/.ldrelease/config.yml000066400000000000000000000004001430321663200203020ustar00rootroot00000000000000version: 2 jobs: - docker: image: golang:1.18-buster template: name: go branches: - name: v3 - name: v2 - name: v1 publications: - url: https://pkg.go.dev/github.com/launchdarkly/go-jsonstream/v3 description: documentation go-jsonstream-3.0.0/CHANGELOG.md000066400000000000000000000021521430321663200161130ustar00rootroot00000000000000# Change log All notable changes to the project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). ## [3.0.0] - 2022-08-29 This release drops compatibility with Go 1.17 and below, and changes the import path from `github.com/launchdarkly/go-jsonstream/v2` to `github.com/launchdarkly/go-jsonstream/v3`. There are no other changes. ## [2.0.0] - 2022-03-18 This release drops compatibility with Go 1.15 and below, and changes the import path from `gopkg.in/launchdarkly/go-jsonstream.v1` to `github.com/launchdarkly/go-jsonstream/v2`. There are no functional changes. ## [1.0.1] - 2021-06-03 ### Fixed: - Parsing of numeric values in the default implementation was broken for numbers that have an exponent but do not have a decimal (such as 1e-5, as opposed to 1.0e-5). For such numbers, the parser was returning an integer value based on misusing the ASCII values of the non-digit characters as if they were digits, e.g. 1e-5 was interpreted as 88035. This bug did not occur in the EasyJSON implementation of the parser. ## [1.0.0] - 2020-12-17 Initial release of this library. go-jsonstream-3.0.0/CONTRIBUTING.md000066400000000000000000000074141430321663200165410ustar00rootroot00000000000000# Contributing to this project ## Submitting bug reports and feature requests The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/go-jsonstream/issues) in tis repository. Bug reports and feature requests specific to this project should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. ## Submitting pull requests We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. ## Build instructions ### Prerequisites This project should be built against the lowest supported Go version as described in [README.md](./README.md). ### Building To build the project without running any tests: ``` make ``` If you wish to clean your working directory between builds, you can clean it by running: ``` make clean ``` To run the linter: ``` make lint ``` ### Testing To build and run all unit tests, for the default implementation and also the easyjson implementation: ``` make test make test-easyjson ``` To run benchmarks: ``` make benchmarks make benchmarks-easyjson ``` ## Coding best practices ### Test coverage It is important to keep unit test coverage as close to 100% as possible in this project. You can view the latest code coverage report in CircleCI, as `coverage.html` and `coverage.txt` in the artifacts. You can also generate this information locally with `make test-coverage`. The build will fail if there are any uncovered blocks of code, unless you explicitly add an override by placing a comment that starts with `// COVERAGE` somewhere within that block. Sometimes a gap in coverage is unavoidable, usually because the compiler requires us to provide a code path for some condition that in practice can't happen and can't be tested. Exclude these paths with a `// COVERAGE` comment. ### Avoid heap allocations For performance and to avoid unwanted heap churn, it is highly desirable to avoid allocating data on the heap if it could instead be passed as a value type. Go's memory model uses a mix of stack and heap allocations, with the compiler transparently choosing the most appropriate strategy based on various type and scope rules. It is always preferable, when possible, to keep ephemeral values on the stack rather than on the heap to avoid creating extra work for the garbage collector. - The most obvious rule is that anything explicitly allocated by reference (`x := &SomeType{}`), or returned by reference (`return &x`), will be allocated on the heap. Avoid this unless the object has mutable state that must be shared. - Casting a value type to an interface causes it to be allocated on the heap, since an interface is really a combination of a type identifier and a hidden pointer. - A closure that references any variables outside of its scope (including the method receiver, if it is inside a method) causes an object to be allocated on the heap containing the values or addresses of those variables. - Treating a method as an anonymous function (`myFunc := someReceiver.SomeMethod`) is equivalent to a closure. Allocations are counted in the benchmark output: "5 allocs/op" means that a total of 5 heap objects were allocated during each run of the benchmark. This does not mean that the objects were retained, only that they were allocated at some point. For methods that should be guaranteed _not_ to do any heap allocations, the corresponding benchmarks should have names ending in `NoAlloc`. The `make benchmarks` target will automatically fail if allocations are detected in any benchmarks that have this name suffix. go-jsonstream-3.0.0/LICENSE.txt000066400000000000000000000010541430321663200161250ustar00rootroot00000000000000Copyright 2020 Catamorphic, Co. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.go-jsonstream-3.0.0/Makefile000066400000000000000000000064021430321663200157440ustar00rootroot00000000000000 GOLANGCI_LINT_VERSION=v1.48.0 LINTER=./bin/golangci-lint LINTER_VERSION_FILE=./bin/.golangci-lint-version-$(GOLANGCI_LINT_VERSION) ALL_SOURCES := $(shell find * -type f -name "*.go") COVERAGE_PROFILE_RAW=./build/coverage_raw.out COVERAGE_PROFILE_RAW_HTML=./build/coverage_raw.html COVERAGE_PROFILE_FILTERED=./build/coverage.out COVERAGE_PROFILE_FILTERED_HTML=./build/coverage.html COVERAGE_ENFORCER_FLAGS=-skipcode "// COVERAGE" -packagestats -filestats -showcode TEST_BINARY=./build/go-jsonstream.test TEST_COMPILE_LOG=./build/compile.out ALLOCATIONS_LOG=./build/allocations.out EASYJSON_TAG=-tags launchdarkly_easyjson .PHONY: all build build-easyjson clean test test-easyjson test-coverage lint all: build build-easyjson build: go build ./... build-easyjson: go build $(EASYJSON_TAG) ./... clean: go clean test: build go test -count 1 ./... test-easyjson: build-easyjson go test $(EASYJSON_TAG) -count 1 ./... test-coverage: $(COVERAGE_PROFILE_RAW) go run github.com/launchdarkly-labs/go-coverage-enforcer@latest $(COVERAGE_ENFORCER_FLAGS) -outprofile $(COVERAGE_PROFILE_FILTERED) $(COVERAGE_PROFILE_RAW) go tool cover -html $(COVERAGE_PROFILE_FILTERED) -o $(COVERAGE_PROFILE_FILTERED_HTML) go tool cover -html $(COVERAGE_PROFILE_RAW) -o $(COVERAGE_PROFILE_RAW_HTML) $(COVERAGE_PROFILE_RAW): $(ALL_SOURCES) @mkdir -p ./build go test -coverprofile $(COVERAGE_PROFILE_RAW) ./... >/dev/null benchmarks: build @mkdir -p ./build go test -benchmem '-run=^$$' '-bench=.*' ./... | tee build/benchmarks.out @if grep $(ALLOCATIONS_LOG) $(LINTER_VERSION_FILE): rm -f $(LINTER) curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s $(GOLANGCI_LINT_VERSION) touch $(LINTER_VERSION_FILE) lint: $(LINTER_VERSION_FILE) $(LINTER) run ./... go-jsonstream-3.0.0/README.md000066400000000000000000000113201430321663200155560ustar00rootroot00000000000000# LaunchDarkly Streaming JSON for Go [![Circle CI](https://circleci.com/gh/launchdarkly/go-jsonstream.svg?style=shield)](https://circleci.com/gh/launchdarkly/go-jsonstream) [![Documentation](https://img.shields.io/static/v1?label=go.dev&message=reference&color=00add8)](https://pkg.go.dev/gopkg.in/launchdarkly/go-jsonstream.v1) ## Overview The `go-jsonstream` library implements a streaming approach to JSON encoding and decoding which is more efficient than the standard mechanism in `encoding/json`. Unlike `encoding/json` or other reflection-based frameworks, it has no knowledge of structs or other complex types; you must explicitly tell it what values and properties to write or read. It was implemented for the [LaunchDarkly Go SDK](https://github.com/launchdarkly/go-server-sdk) and other LaunchDarkly Go components, but may be useful in other applications. There are two possible implementations, selectable via build tags: 1. A default implementation that has no external dependencies, compatible with all platforms. This performs better than `encoding/json`, but not as well as the other two below. 2. An implementation that uses the low-level tokenizing and output functions from the [easyjson](https://github.com/mailru/easyjson) library (but without the code generation mechanism that easyjson also provides). This is used if you enable the build tag `launchdarkly_easyjson`. Although the easyjson implementation is the fastest, it is opt-in rather than being the default, for two reasons: * By default, easyjson uses Go's `unsafe` package, which may be undesirable or not allowed depending on your runtime environment. Easyjson disables the use of `unsafe` if you set the build tag `easyjson_nounsafe` or `appengine`. * Although easyjson is widely used, at this time it does not have a stable 1.x version. The design of `go-jsonstream` allows encoding/decoding logic to be written against the same API without needing to know whether the easyjson implementation will be used at build time or not. There is an adapter (also conditionally compiled with the `launchdarkly_easyjson` tag) to allow `go-jsonstream` to plug directly into a `jlexer.Lexer` or `jwriter.Writer` that is being used to read or write some other type with easyjson. The unit tests for `go-jsonstream` define a common test suite that is run against the default implementation and the easyjson implementation, to verify that their behavior is consistent across a large number of permutations of possible JSON inputs and outputs. ## Supported Go versions This version of the project requires a Go version of 1.18 or higher. ## Contributing We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK. ## About LaunchDarkly * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. * Grant access to certain features based on user attributes, like payment plan (eg: users on the β€˜gold’ plan get access to more features than users in the β€˜silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. * Explore LaunchDarkly * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies go-jsonstream-3.0.0/go.mod000066400000000000000000000005541430321663200154140ustar00rootroot00000000000000module github.com/launchdarkly/go-jsonstream/v3 go 1.18 require ( github.com/mailru/easyjson v0.7.6 github.com/stretchr/testify v1.6.1 ) require ( github.com/davecgh/go-spew v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) go-jsonstream-3.0.0/go.sum000066400000000000000000000025301430321663200154350ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= go-jsonstream-3.0.0/internal/000077500000000000000000000000001430321663200161165ustar00rootroot00000000000000go-jsonstream-3.0.0/internal/commontest/000077500000000000000000000000001430321663200203065ustar00rootroot00000000000000go-jsonstream-3.0.0/internal/commontest/assertions.go000066400000000000000000000011341430321663200230260ustar00rootroot00000000000000package commontest import ( "errors" "fmt" "reflect" ) // These functions provide a simple mechanism for returning errors as errors instead of using // assert or require. func AssertEqual(expected, actual interface{}) error { if !reflect.DeepEqual(expected, actual) { return fmt.Errorf("expected %s, got %s", expected, actual) } return nil } func AssertTrue(value bool, failureMessage string) error { if !value { return errors.New(failureMessage) } return nil } func AssertNoErrors(errs ...error) error { for _, err := range errs { if err != nil { return err } } return nil } go-jsonstream-3.0.0/internal/commontest/common_test_data.go000066400000000000000000000036761430321663200241710ustar00rootroot00000000000000package commontest import ( "bytes" "encoding/json" "fmt" ) type ExampleStruct struct { StringField string `json:"string"` IntField int `json:"int"` OptBoolAsInterfaceField interface{} `json:"optBool"` } const ( ExampleStructStringFieldName = "string" ExampleStructIntFieldName = "int" ExampleStructOptBoolAsInterfaceFieldName = "optBool" ) var ( ExampleStructData = []byte(`{"string":"s","int":3,"optBool":true}`) ExampleStructValue = ExampleStruct{StringField: "s", IntField: 3, OptBoolAsInterfaceField: true} ExampleStructRequiredFieldNames = []string{ExampleStructStringFieldName, ExampleStructIntFieldName} ) func MakeBools() []bool { ret := make([]bool, 0, 100) for i := 0; i < 50; i++ { ret = append(ret, false, true) } return ret } func MakeBoolsJSON(bools []bool) []byte { var buf bytes.Buffer buf.WriteRune('[') for i, val := range bools { if i > 0 { buf.WriteRune(',') } buf.WriteString(fmt.Sprintf("%t", val)) } buf.WriteRune(']') return buf.Bytes() } func MakeStrings() []string { ret := make([]string, 0, 100) for i := 0; i < 50; i++ { ret = append(ret, fmt.Sprintf("value%d", i)) ret = append(ret, fmt.Sprintf("value\twith\n\"escaped chars\"%d", i)) } return ret } func MakeStringsJSON(strings []string) []byte { var buf bytes.Buffer buf.WriteRune('[') for i, val := range strings { if i > 0 { buf.WriteRune(',') } data, _ := json.Marshal(val) _, _ = buf.Write(data) } buf.WriteRune(']') return buf.Bytes() } func MakeStructs() []ExampleStruct { ret := make([]ExampleStruct, 0, 100) for i := 0; i < 100; i++ { ret = append(ret, ExampleStruct{ StringField: fmt.Sprintf("string%d", i), IntField: i * 10, OptBoolAsInterfaceField: i%2 == 1, }) } return ret } func MakeStructsJSON(structs []ExampleStruct) []byte { bytes, _ := json.Marshal(structs) return bytes } go-jsonstream-3.0.0/internal/commontest/interfaces.go000066400000000000000000000050071430321663200227620ustar00rootroot00000000000000package commontest // TestContext is an abstraction used by ReaderTestSuite and WriterTestSuite. type TestContext interface { // JSONData returns either (for readers) the input data that was passed in when the TestContext // was created, or (for writers) all of the output that has been produced so far. JSONData() []byte } // Action is an arbitrary action that can be executed during a test. For readers, this normally // consists of trying to read some type of value from the input, and asserting that no error occurred // and that the expected value was found. For writers, it consists of trying to write something to // the output. // // All test assertions should return early on any non-nil error. type Action func(c TestContext) error // PropertyAction is used in the context of a JSON object value, describing a property name and the // Action for reading or writing the property value. type PropertyAction struct { Name string Action Action } // ValueVariant is an optional identifier that ValueTestFactory can use to make the tests produce // multiple variations of value tests. See ValueTestFactory.Variants. type ValueVariant string const ( // This variant means that the reader will try to consume a JSON value without regard to its type, // or the writer will write it as raw JSON data. UntypedVariant ValueVariant = "any:" // This variant means that the reader will try to recursively skip past a JSON value of any type. SkipValueVariant ValueVariant = "skip:" ) // ValueTestFactory is an interface for producing specific reader/writer test actions. To test any // reader or writer with ReaderTestSuite or WriterTestSuite, provide an implementation of this // interface that performs the specified actions. type ValueTestFactory interface { EOF() Action Value(value AnyValue, variant ValueVariant) Action Variants(value AnyValue) []ValueVariant } // ReadErrorTestFactory is an interface for use with ReaderTestSuite to generate expectations about // how errors are reported. type ReadErrorTestFactory interface { ExpectEOFError(err error) error ExpectWrongTypeError(err error, expectedType ValueKind, variant ValueVariant, gotType ValueKind) error ExpectSyntaxError(err error) error } type ValueKind int const ( NullValue ValueKind = iota BoolValue ValueKind = iota NumberValue ValueKind = iota StringValue ValueKind = iota ArrayValue ValueKind = iota ObjectValue ValueKind = iota ) type AnyValue struct { Kind ValueKind Bool bool Number float64 String string Array []Action Object []PropertyAction } go-jsonstream-3.0.0/internal/commontest/package_info.go000066400000000000000000000013171430321663200232450ustar00rootroot00000000000000// Package commontest provides test logic for JSON reading and writing. // // To ensure that the go-jsonstream types perform correctly with a wide range of inputs and outputs, // we generate many permutations (single scalar values of various types; numbers in different formats; // strings with or without escape characters at different positions; arrays and objects with different // numbers of elements/properties) which are tested for both readers and writers. For readers, we also // test various permutations of invalid input. // // Reader and writer tests are run against the high-level APIs (Reader, Writer) and the default // implementations of the low-level APIs (tokenReader, tokenWriter). package commontest go-jsonstream-3.0.0/internal/commontest/reader_test_suite.go000066400000000000000000000036441430321663200243560ustar00rootroot00000000000000package commontest import ( "strings" "testing" "github.com/stretchr/testify/require" ) // ReaderTestSuite runs a standard set of tests against some implementation of JSON reading. // This allows us to test both jreader.Reader and the low-level tokenizer jreader.tokenReader // with many permutations of valid and invalid input. type ReaderTestSuite struct { // ContextFactory must be provided by the caller to create an implementation of TestContext for // running a parsing test on the specified JSON input. This should include whatever parser // object will be used by the Actions that the ValueTestFactory creates. ContextFactory func(input []byte) TestContext // ValueTestFactory must be provided by the caller to create implementations of Action for // various JSON value types. ValueTestFactory ValueTestFactory // ReadErrorTestFactory must be provided by the caller to define expectations about error // reporting for invalid input. ReadErrorTestFactory ReadErrorTestFactory } // Run runs the test suite. func (s ReaderTestSuite) Run(t *testing.T) { tf := testFactory{ valueTestFactory: s.ValueTestFactory, readErrorTestFactory: s.ReadErrorTestFactory, encodingBehavior: encodingBehavior{ forParsing: true, }, } var testDefs testDefs testDefs = append(testDefs, tf.MakeAllValueTests()...) testDefs = append(testDefs, tf.MakeAllReadErrorTests()...) whitespaceOptions := MakeWhitespaceOptions() whitespaceOptions[""] = "" for _, td := range testDefs { for wsName, wsValue := range whitespaceOptions { testName := td.name if wsName != "" { testName += " [with whitespace: " + wsName + "]" } t.Run(testName, func(t *testing.T) { input := wsValue + strings.Join(td.encoding, wsValue) + wsValue t.Cleanup(func() { if t.Failed() { t.Logf("JSON input was: `%s`", input) } }) c := s.ContextFactory([]byte(input)) require.NoError(t, td.action(c)) }) } } } go-jsonstream-3.0.0/internal/commontest/test_factory.go000066400000000000000000000175721430321663200233570ustar00rootroot00000000000000package commontest import ( "fmt" "strings" ) // This file contains the logic for generating all the valid JSON permutations and error conditions // that will be tested by ReaderTestSuite and WriterTestSuite. type testFactory struct { valueTestFactory ValueTestFactory readErrorTestFactory ReadErrorTestFactory encodingBehavior encodingBehavior } // This struct associates a test Action with some JSON data. type testDef struct { // A descriptive name, used in test output. name string // For readers, encoding is the JSON input data; for writers, the expected JSON output. It is // defined as a list of substrings, with the expectation that there may be any amount of // whitespace between each substring. encoding []string // The test action for either reading or writing this piece of JSON. action Action } type testDefs []testDef func (td testDef) then(next testDef) testDef { return testDef{ name: td.name + ", " + next.name, encoding: append(td.encoding, next.encoding...), action: func(ctx TestContext) error { if err := td.action(ctx); err != nil { return err } return next.action(ctx) }, } } func (tds testDefs) then(next testDef) testDefs { ret := make(testDefs, 0, len(tds)) for _, td := range tds { ret = append(ret, td.then(next)) } return ret } func (f testFactory) MakeAllValueTests() testDefs { ret := testDefs{} eofTest := testDef{name: "EOF", action: f.valueTestFactory.EOF()} ret = append(ret, f.makeScalarValueTests(true).then(eofTest)...) ret = append(ret, f.makeArrayTests().then(eofTest)...) ret = append(ret, f.makeObjectTests().then(eofTest)...) return ret } func maybeVariants(vs []ValueVariant) []ValueVariant { if len(vs) == 0 { return []ValueVariant{""} } return vs } func (f testFactory) MakeAllReadErrorTests() testDefs { ret := testDefs{} addErrors := func(tds testDefs) { for _, td := range tds { for i, enc := range td.encoding { if enc == "" { // this means we want to force an unexpected EOF td.encoding = td.encoding[0:i] break } } ret = append(ret, td) } } addErrors(f.makeScalarValueReadErrorTests()) return ret } func (f testFactory) makeScalarValueTests(allPermutations bool) testDefs { ret := testDefs{} values := f.makeScalarValues(allPermutations) for _, tv := range values { variants := maybeVariants(f.valueTestFactory.Variants(tv.value)) for _, variant := range variants { name := tv.name if variant != "" { name = string(variant) + " " + name } td := testDef{ name: name, encoding: []string{tv.encoding}, action: f.valueTestFactory.Value(tv.value, variant), } ret = append(ret, td) } } return ret } func (f testFactory) makeScalarValueReadErrorTests() testDefs { ret := testDefs{} values := f.makeScalarValues(false) oneVariant := []ValueVariant{""} for _, testValue := range values { tv := testValue variants := f.valueTestFactory.Variants(tv.value) if variants == nil { variants = oneVariant } for _, variant := range variants { v := variant name := tv.name if v != "" { name = string(v) + " " + name } testAction := f.valueTestFactory.Value(tv.value, variant) // error: want a value, got a value of some other type if v != UntypedVariant && v != SkipValueVariant { for _, wrongValue := range f.makeScalarValues(false) { wv := wrongValue if wv.value.Kind == tv.value.Kind { continue } ret = append(ret, testDef{ name: fmt.Sprintf("%s (but got %s)", name, wv.name), encoding: []string{wv.encoding}, action: func(c TestContext) error { return f.readErrorTestFactory.ExpectWrongTypeError(testAction(c), tv.value.Kind, v, wv.value.Kind) }, }) } if tv.value.Kind != NullValue { ret = append(ret, testDef{ name: fmt.Sprintf("%s (but got array)", name), encoding: []string{"[]"}, action: func(c TestContext) error { return f.readErrorTestFactory.ExpectWrongTypeError(testAction(c), tv.value.Kind, v, ArrayValue) }, }) ret = append(ret, testDef{ name: fmt.Sprintf("%s (but got object)", name), encoding: []string{"{}"}, action: func(c TestContext) error { return f.readErrorTestFactory.ExpectWrongTypeError(testAction(c), tv.value.Kind, v, ObjectValue) }, }) } } // error: want a value, got some invalid JSON for _, badThing := range []struct { name string encoding string }{ {"invalid identifier", "bad"}, {"unknown delimiter", "+"}, {"unexpected end array", "]"}, {"unexpected object", "}"}, } { ret = append(ret, testDef{ name: fmt.Sprintf("%s (but got %s)", name, badThing.name), encoding: []string{badThing.encoding}, action: func(c TestContext) error { return f.readErrorTestFactory.ExpectSyntaxError(testAction(c)) }, }) } ret = append(ret, testDef{ name: fmt.Sprintf("%s (but got unexpected EOF)", name), encoding: []string{""}, action: func(c TestContext) error { return f.readErrorTestFactory.ExpectEOFError(testAction(c)) }, }) } } return ret } func (f testFactory) makeScalarValues(allPermutations bool) []testValue { var values []testValue values = append(values, testValue{ name: "null", encoding: "null", value: AnyValue{Kind: NullValue}, }) values = append(values, makeBoolTestValues()...) values = append(values, makeNumberTestValues(f.encodingBehavior)...) values = append(values, makeStringTestValues(f.encodingBehavior, allPermutations)...) return values } func (f testFactory) makeArrayTests() testDefs { ret := testDefs{} for elementCount := 0; elementCount <= 2; elementCount++ { for _, contents := range f.makeValueListsOfLength(elementCount) { var names []string var encoding = []string{"["} var actions []Action for i, td := range contents { names = append(names, td.name) if i > 0 { encoding = append(encoding, ",") } encoding = append(encoding, td.encoding...) actions = append(actions, td.action) } encoding = append(encoding, "]") value := AnyValue{Kind: ArrayValue, Array: actions} for _, variant := range maybeVariants(f.valueTestFactory.Variants(value)) { arrayTest := testDef{ name: "array(" + strings.Join(names, ", ") + ")", encoding: encoding, action: f.valueTestFactory.Value(value, variant), } ret = append(ret, arrayTest) } } } return ret } func (f testFactory) makeObjectTests() testDefs { ret := testDefs{} for propertyCount := 0; propertyCount <= 2; propertyCount++ { for _, contents := range f.makeValueListsOfLength(propertyCount) { var names []string var encoding = []string{"{"} var propActions []PropertyAction for i, td := range contents { propName := fmt.Sprintf("prop%d", i) names = append(names, fmt.Sprintf("%s: %s", propName, td.name)) if i > 0 { encoding = append(encoding, ",") } encoding = append(encoding, fmt.Sprintf(`"%s"`, propName)) encoding = append(encoding, ":") encoding = append(encoding, td.encoding...) propActions = append(propActions, PropertyAction{Name: propName, Action: td.action}) } encoding = append(encoding, "}") value := AnyValue{Kind: ObjectValue, Object: propActions} for _, variant := range maybeVariants(f.valueTestFactory.Variants(value)) { objectTest := testDef{ name: "object(" + strings.Join(names, ", ") + ")", encoding: encoding, action: f.valueTestFactory.Value(value, variant), } ret = append(ret, objectTest) } } } return ret } func (f testFactory) makeValueListsOfLength(count int) []testDefs { if count == 0 { return []testDefs{testDefs{}} } previousLists := f.makeValueListsOfLength(count - 1) ret := []testDefs{} for _, previous := range previousLists { for _, elementTest := range f.makeScalarValueTests(false) { ret = append(ret, append(previous, elementTest)) } } return ret } go-jsonstream-3.0.0/internal/commontest/test_values.go000066400000000000000000000120631430321663200231750ustar00rootroot00000000000000package commontest import ( "fmt" "strconv" ) // This file is used by test_factory.go to define a standard set of JSON scalar test values. type encodingBehavior struct { encodeAsHex func(rune) bool forParsing bool } type testValue struct { name string encoding string value AnyValue } type numberTestValueBase struct { name string val float64 encoding string simplestEncoding string } type stringTestValueBase struct { name string val string encoding string } func makeBoolTestValues() []testValue { return []testValue{ {"bool true", "true", AnyValue{Kind: BoolValue, Bool: true}}, {"bool false", "false", AnyValue{Kind: BoolValue, Bool: false}}, } } func makeNumberTestValues(encodingBehavior encodingBehavior) []testValue { var ret []testValue for _, v := range []numberTestValueBase{ {"zero", 0, "0", ""}, {"int", 3, "3", ""}, {"int negative", -3, "-3", ""}, {"int large", 1603312301195, "1603312301195", ""}, // enough magnitude for a millisecond timestamp {"float", 3.5, "3.5", ""}, {"float negative", -3.5, "-3.5", ""}, {"float with exp and decimal", 3500, "3.5e3", "3500"}, {"float with Exp and decimal", 3500, "3.5E3", "3500"}, {"float with exp+ and decimal", 3500, "3.5e+3", "3500"}, {"float with exp- and decimal", 0.0035, "3.5e-3", "0.0035"}, {"float with exp but no decimal", 5000, "5e3", "5000"}, {"float with Exp but no decimal", 5000, "5E3", "5000"}, {"float with exp+ but no decimal", 5000, "5e+3", "5000"}, {"float with exp- but no decimal", 0.005, "5e-3", "0.005"}, } { enc := v.encoding if !encodingBehavior.forParsing && v.simplestEncoding != "" { enc = v.simplestEncoding } ret = append(ret, testValue{"number " + v.name, enc, AnyValue{Kind: NumberValue, Number: v.val}}) } return ret } func makeStringTestValues(encodingBehavior encodingBehavior, allPermutations bool) []testValue { base := []stringTestValueBase{ {name: "empty", val: "", encoding: `""`}, {name: "simple", val: "abc", encoding: `"abc"`}, } allEscapeTests := []stringTestValueBase{} if allPermutations { baseEscapeTests := []stringTestValueBase{ {val: `"`, encoding: `\"`}, {val: `\`, encoding: `\\`}, {val: "\x05", encoding: `\u0005`}, {val: "\x1c", encoding: `\u001c`}, {val: "πŸ¦œπŸ¦„πŸ˜‚πŸ§ΆπŸ˜» yes", encoding: "πŸ¦œπŸ¦„πŸ˜‚πŸ§ΆπŸ˜» yes"}, // unescaped multi-byte characters are allowed } addControlChar := func(str, shortEncoding string) { hex := strconv.FormatInt(int64(str[0]), 16) if len(hex) == 1 { hex = "0" + hex } encodeAsHex := false if encodingBehavior.encodeAsHex != nil { encodeAsHex = encodingBehavior.encodeAsHex(rune(str[0])) } if !encodingBehavior.forParsing && !encodeAsHex { baseEscapeTests = append(baseEscapeTests, stringTestValueBase{val: str, encoding: shortEncoding}) } if encodingBehavior.forParsing || encodeAsHex { baseEscapeTests = append(baseEscapeTests, stringTestValueBase{val: str, encoding: `\u00` + hex}) } } addControlChar("\b", `\b`) addControlChar("\t", `\t`) addControlChar("\n", `\n`) addControlChar("\f", `\f`) addControlChar("\r", `\r`) if encodingBehavior.forParsing { // These escapes are not used when writing, but may be encountered when parsing baseEscapeTests = append(baseEscapeTests, stringTestValueBase{val: "/", encoding: `\/`}) baseEscapeTests = append(baseEscapeTests, stringTestValueBase{val: "γ‚‚", encoding: `\u3082`}) } for _, et := range baseEscapeTests { allEscapeTests = append(allEscapeTests, et) addTransformed := func(valFn func(string) string, encFn func(string) string) { tt := stringTestValueBase{ val: valFn(et.val), encoding: encFn(et.encoding), } allEscapeTests = append(allEscapeTests, tt) } for _, f := range []string{"%sabcd", "abcd%s", "ab%scd"} { addTransformed(func(s string) string { return fmt.Sprintf(f, s) }, func(s string) string { return fmt.Sprintf(f, s) }) } for _, et2 := range baseEscapeTests { for _, f := range []string{"%s%sabcd", "ab%s%scd", "a%sbc%sd", "abcd%s%s"} { addTransformed(func(s string) string { return fmt.Sprintf(f, s, et2.val) }, func(s string) string { return fmt.Sprintf(f, s, et2.encoding) }) } } } } else { // When we're testing nested data structures, we don't need to cover all those permutations of // escape sequences-- we can assume that the same string encoding logic applies as it would for // a single value. allEscapeTests = []stringTestValueBase{{val: "simple\tescape", encoding: `simple\tescape`}} } for i, et := range allEscapeTests { st := stringTestValueBase{ name: fmt.Sprintf("with escapes %d", i+1), val: et.val, encoding: `"` + et.encoding + `"`, } base = append(base, st) } ret := make([]testValue, 0, len(base)) for _, b := range base { ret = append(ret, testValue{name: "string " + b.name, encoding: b.encoding, value: AnyValue{Kind: StringValue, String: b.val}}) } return ret } func MakeWhitespaceOptions() map[string]string { return map[string]string{"spaces": " ", "tab": "\t", "newline": "\n"} } go-jsonstream-3.0.0/internal/commontest/writer_test_suite.go000066400000000000000000000040331430321663200244210ustar00rootroot00000000000000package commontest import ( "regexp" "testing" "github.com/stretchr/testify/require" ) // WriterTestSuite runs a standard set of tests against some implementation of JSON writing. // This allows us to test both jwriter.Writer and the low-level JSON formatter jwriter.tokenWriter // with many permutations of output data. type WriterTestSuite struct { // ContextFactory must be provided by the caller to create an implementation of TestContext for // running a writing test on some set of JSON data. This should include whatever writer object // will be used by the Actions that the ValueTestFactory creates. ContextFactory func() TestContext // ValueTestFactory must be provided by the caller to create implementations of Action for // various JSON value types. ValueTestFactory ValueTestFactory // EncodeAsHex must be provided by the caller to define expectations about whether this writer // will use a \uNNNN escape sequence for the specified Unicode character. There is no single // correct answer for all implementations, since JSON allows characters to be escaped in // several ways and also allows unescaped multi-byte characters. EncodeAsHex func(rune) bool } // Run runs the test suite. func (s WriterTestSuite) Run(t *testing.T) { tf := testFactory{ valueTestFactory: s.ValueTestFactory, encodingBehavior: encodingBehavior{ encodeAsHex: s.EncodeAsHex, }, } tds := tf.MakeAllValueTests() for _, td := range tds { t.Run(td.name, func(t *testing.T) { c := s.ContextFactory() t.Cleanup(func() { if t.Failed() { t.Logf("JSON output: `%s`", string(c.JSONData())) } }) require.NoError(t, td.action(c)) output := string(c.JSONData()) require.Regexp(t, makeOutputRegex(td.encoding), output) }) } } // Make a regex that will allow any amount of whitespace between the matched substrings. func makeOutputRegex(outputParts []string) *regexp.Regexp { regex := "\\w*" for _, outputPart := range outputParts { regex += regexp.QuoteMeta(outputPart) regex += "\\w*" } return regexp.MustCompile(regex) } go-jsonstream-3.0.0/jreader/000077500000000000000000000000001430321663200157165ustar00rootroot00000000000000go-jsonstream-3.0.0/jreader/errors.go000066400000000000000000000061311430321663200175620ustar00rootroot00000000000000package jreader import ( "encoding/json" "fmt" "reflect" ) const ( errMsgBadArrayItem = "expected comma or end of array" errMsgBadObjectItem = "expected comma or end of object" errMsgDataAfterEnd = "unexpected data after end of JSON value" errMsgExpectedColon = "expected colon after property name" errMsgInvalidNumber = "invalid numeric value" errMsgInvalidString = "unterminated or invalid string value" errMsgUnexpectedChar = "unexpected character" errMsgUnexpectedSymbol = "unexpected symbol" ) // SyntaxError is returned by Reader if the input is not well-formed JSON. type SyntaxError struct { // Message is a descriptive message. Message string // Offset is the approximate character index within the input where the error occurred. Offset int // Value, if not empty, is the token that caused the error. Value string } // TypeError is returned by Reader if the type of JSON value that was read did not // match what the caller requested. type TypeError struct { // Expected is the type of JSON value that the caller requested. Expected ValueKind // Actual is the type of JSON value that was found. Actual ValueKind // Nullable is true if the caller indicated that a null value was acceptable in this context. Nullable bool // Offset is the approximate character index within the input where the error occurred. Offset int } // RequiredPropertyError is returned by Reader if a JSON object did not contain a property that // was designated as required (by using ObjectState.WithRequiredProperties). type RequiredPropertyError struct { // Name is the name of a required object property that was not found. Name string // Offset is the approximate character index within the input where the error occurred // (at or near the end of the JSON object). Offset int } // Error returns a description of the error. func (e SyntaxError) Error() string { if e.Value != "" { return fmt.Sprintf("%s at position %d (%q)", e.Message, e.Offset, e.Value) } return fmt.Sprintf("%s at position %d", e.Message, e.Offset) } // Error returns a description of the error. func (e TypeError) Error() string { if e.Nullable { return fmt.Sprintf("expected %s or null, got %s at position %d", e.Expected, e.Actual, e.Offset) } return fmt.Sprintf("expected %s, got %s at position %d", e.Expected, e.Actual, e.Offset) } // Error returns a description of the error. func (e RequiredPropertyError) Error() string { return fmt.Sprintf("a required property %q was missing from a JSON object at position %d", e.Name, e.Offset) } // ToJSONError converts errors defined by the jreader package into the corresponding error types defined // by the encoding/json package, if any. The target parameter, if not nil, is used to determine the // target value type for json.UnmarshalTypeError. func ToJSONError(err error, target interface{}) error { switch e := err.(type) { case SyntaxError: return &json.SyntaxError{ Offset: int64(e.Offset), } case TypeError: return &json.UnmarshalTypeError{ Value: e.Expected.String(), Type: reflect.TypeOf(target), Offset: int64(e.Offset), } } return err } go-jsonstream-3.0.0/jreader/errors_test.go000066400000000000000000000037271430321663200206310ustar00rootroot00000000000000package jreader import ( "encoding/json" "errors" "reflect" "testing" "github.com/stretchr/testify/assert" ) func TestSyntaxError(t *testing.T) { e1 := SyntaxError{Message: "xyz", Offset: 2} assert.Equal(t, "xyz at position 2", e1.Error()) e2 := SyntaxError{Message: "xyz", Offset: 2, Value: "abc"} assert.Equal(t, `xyz at position 2 ("abc")`, e2.Error()) } func TestTypeError(t *testing.T) { assert.Equal(t, "expected boolean, got string at position 2", TypeError{Expected: BoolValue, Actual: StringValue, Offset: 2}.Error()) assert.Equal(t, "expected boolean or null, got string at position 2", TypeError{Expected: BoolValue, Actual: StringValue, Offset: 2, Nullable: true}.Error()) assert.Equal(t, "expected null, got boolean at position 2", TypeError{Expected: NullValue, Actual: BoolValue, Offset: 2}.Error()) assert.Equal(t, "expected null, got number at position 2", TypeError{Expected: NullValue, Actual: NumberValue, Offset: 2}.Error()) assert.Equal(t, "expected null, got string at position 2", TypeError{Expected: NullValue, Actual: StringValue, Offset: 2}.Error()) assert.Equal(t, "expected null, got array at position 2", TypeError{Expected: NullValue, Actual: ArrayValue, Offset: 2}.Error()) assert.Equal(t, "expected null, got object at position 2", TypeError{Expected: NullValue, Actual: ObjectValue, Offset: 2}.Error()) assert.Equal(t, "expected null, got unknown token at position 2", TypeError{Expected: NullValue, Actual: 99, Offset: 2}.Error()) } func TestToJSONError(t *testing.T) { e1 := SyntaxError{Message: "xyz", Offset: 2} je1 := ToJSONError(e1, nil) assert.Equal(t, &json.SyntaxError{Offset: 2}, je1) e2 := TypeError{Expected: NumberValue, Actual: StringValue, Offset: 2} someIntValue := 1000 je2 := ToJSONError(e2, someIntValue) assert.Equal(t, &json.UnmarshalTypeError{Value: "number", Offset: 2, Type: reflect.TypeOf(someIntValue)}, je2) e3 := errors.New("some other error") assert.Equal(t, e3, ToJSONError(e3, nil)) } go-jsonstream-3.0.0/jreader/interfaces.go000066400000000000000000000040221430321663200203660ustar00rootroot00000000000000package jreader // AnyValue is returned by Reader.Any() to represent a JSON value of an arbitrary type. type AnyValue struct { // Kind describes the type of the JSON value. Kind ValueKind // Bool is the value if the JSON value is a boolean, or false otherwise. Bool bool // Number is the value if the JSON value is a number, or zero otherwise. Number float64 // String is the value if the JSON value is a string, or an empty string otherwise. String string // Array is an ArrayState that can be used to iterate through the array elements if the JSON // value is an array, or an uninitialized ArrayState{} otherwise. Array ArrayState // Object is an ObjectState that can be used to iterate through the object properties if the // JSON value is an object, or an uninitialized ObjectState{} otherwise. Object ObjectState } // ValueKind defines the allowable value types for Reader.Any. type ValueKind int const ( // NullValue means the value is a null. NullValue ValueKind = iota // BoolValue means the value is a boolean. BoolValue ValueKind = iota // NumberValue means the value is a number. NumberValue ValueKind = iota // StringValue means the value is a string. StringValue ValueKind = iota // ArrayValue means the value is an array. ArrayValue ValueKind = iota // ObjectValue means the value is an object. ObjectValue ValueKind = iota ) // String returns a description of the ValueKind. func (k ValueKind) String() string { switch k { case NullValue: return "null" case BoolValue: return "boolean" case NumberValue: return "number" case StringValue: return "string" case ArrayValue: return "array" case ObjectValue: return "object" default: return "unknown token" } } // Readable is an interface for types that can read their data from a Reader. type Readable interface { // ReadFromJSONReader attempts to read the object's state from a Reader. // // This method does not need to return an error value because Reader remembers when it // has encountered an error. ReadFromJSONReader(*Reader) } go-jsonstream-3.0.0/jreader/json_unmarshal_comparative_benchmark_test.go000066400000000000000000000077241430321663200267450ustar00rootroot00000000000000package jreader import ( "encoding/json" "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" ) // These benchmarks perform equivalent actions to the ones in reader_benchmark_test.go, but using // the default reflection-based mechanism from the json/encoding package, so we can see how much // less efficient that is than our default implementation and the easyjson implementation. func BenchmarkJSONUnmarshalComparatives(b *testing.B) { b.Run("Null", benchmarkReadNullJSONUnmarshal) b.Run("Boolean", benchmarkReadBooleanJSONUnmarshal) b.Run("NumberInt", benchmarkReadNumberIntJSONUnmarshal) b.Run("NumberFloat", benchmarkReadNumberFloatJSONUnmarshal) b.Run("String", benchmarkReadStringJSONUnmarshal) b.Run("ArrayOfBools", benchmarkReadArrayOfBoolsJSONUnmarshal) b.Run("ArrayOfStrings", benchmarkReadArrayOfStringsJSONUnmarshal) b.Run("Object", benchmarkReadObjectJSONUnmarshal) b.Run("ArrayOfObjects", benchmarkReadArrayOfObjectsJSONUnmarshal) } func benchmarkReadNullJSONUnmarshal(b *testing.B) { data := []byte("null") var expected interface{} = nil b.ResetTimer() for i := 0; i < b.N; i++ { var val interface{} if err := json.Unmarshal(data, &val); err != nil { b.Error(err) b.FailNow() } if val != expected { b.FailNow() } } } func benchmarkReadBooleanJSONUnmarshal(b *testing.B) { data := []byte("true") expected := true b.ResetTimer() for i := 0; i < b.N; i++ { var val bool if err := json.Unmarshal(data, &val); err != nil { b.Error(err) b.FailNow() } if val != expected { b.FailNow() } } } func benchmarkReadNumberIntJSONUnmarshal(b *testing.B) { data := []byte("1234") expected := 1234 b.ResetTimer() for i := 0; i < b.N; i++ { var val int if err := json.Unmarshal(data, &val); err != nil { b.Error(err) b.FailNow() } if val != expected { b.FailNow() } } } func benchmarkReadNumberFloatJSONUnmarshal(b *testing.B) { data := []byte("1234.5") expected := 1234.5 b.ResetTimer() for i := 0; i < b.N; i++ { var val float64 if err := json.Unmarshal(data, &val); err != nil { b.Error(err) b.FailNow() } if val != expected { b.FailNow() } } } func benchmarkReadStringJSONUnmarshal(b *testing.B) { data := []byte(`"abc"`) expected := "abc" b.ResetTimer() for i := 0; i < b.N; i++ { var val string if err := json.Unmarshal(data, &val); err != nil { b.Error(err) b.FailNow() } if val != expected { b.FailNow() } } } func benchmarkReadArrayOfBoolsJSONUnmarshal(b *testing.B) { expected := commontest.MakeBools() data := commontest.MakeBoolsJSON(expected) b.ResetTimer() for i := 0; i < b.N; i++ { var vals []bool if err := json.Unmarshal(data, &vals); err != nil { b.Error(err) b.FailNow() } if len(vals) < len(expected) { b.FailNow() } } } func benchmarkReadArrayOfStringsJSONUnmarshal(b *testing.B) { expected := commontest.MakeStrings() data := commontest.MakeStringsJSON(expected) b.ResetTimer() for i := 0; i < b.N; i++ { var vals []string if err := json.Unmarshal(data, &vals); err != nil { b.Error(err) b.FailNow() } if len(vals) < len(expected) { b.FailNow() } } } func benchmarkReadObjectJSONUnmarshal(b *testing.B) { for i := 0; i < b.N; i++ { var val ExampleStructWrapper if err := json.Unmarshal(commontest.ExampleStructData, &val); err != nil { b.Error(err) b.FailNow() } if val != ExampleStructWrapper(commontest.ExampleStructValue) { b.FailNow() } } } func benchmarkReadArrayOfObjectsJSONUnmarshal(b *testing.B) { rawStructs := commontest.MakeStructs() data := commontest.MakeStructsJSON(rawStructs) var expected []ExampleStructWrapper for _, rawStruct := range rawStructs { expected = append(expected, ExampleStructWrapper(rawStruct)) } b.ResetTimer() for i := 0; i < b.N; i++ { var values []ExampleStructWrapper if err := json.Unmarshal(data, &values); err != nil { b.Error(err) b.FailNow() } for i, val := range values { if val != expected[i] { b.FailNow() } } } } go-jsonstream-3.0.0/jreader/package_example_test.go000066400000000000000000000003671430321663200224200ustar00rootroot00000000000000package jreader import "fmt" func Example() { r := NewReader([]byte(`"a \"good\" string"`)) s := r.String() if err := r.Error(); err != nil { fmt.Println("error:", err.Error()) } else { fmt.Println(s) } // Output: a "good" string } go-jsonstream-3.0.0/jreader/package_info.go000066400000000000000000000036031430321663200206550ustar00rootroot00000000000000// Package jreader provides an efficient mechanism for reading JSON data sequentially. // // The high-level API for this package, Writer, is designed to facilitate writing custom JSON // marshaling logic concisely and reliably. Output is buffered in memory. // // import ( // "gopkg.in/launchdarkly/jsonstream.v1/jreader" // ) // // type myStruct struct { // value int // } // // func (s *myStruct) ReadFromJSONReader(r *jreader.Reader) { // // reading a JSON object structure like {"value":2} // for obj := r.Object(); obj.Next; { // if string(obj.Name()) == "value" { // s.value = r.Int() // } // } // } // // func ParseMyStructJSON() { // var s myStruct // r := jreader.NewReader([]byte(`{"value":2}`)) // s.ReadFromJSONReader(&r) // fmt.Printf("%+v\n", s) // } // // The underlying low-level token parsing mechanism has two available implementations. The default // implementation has no external dependencies. For interoperability with the easyjson library // (https://github.com/mailru/easyjson), there is also an implementation that delegates to the // easyjson streaming parser; this is enabled by setting the build tag "launchdarkly_easyjson". // Be aware that by default, easyjson uses Go's "unsafe" package (https://pkg.go.dev/unsafe), // which may not be available on all platforms. // // Setting the "launchdarkly_easyjson" tag also adds a new constructor function, // NewReaderFromEasyJSONLexer, allowing Reader-based code to read directly from an existing // EasyJSON jlexer.Lexer. This may be desirable in order to define common unmarshaling logic that // may be used with or without EasyJSON. For example: // // import ( // "github.com/mailru/easyjson/jlexer" // ) // // func (s *myStruct) UnmarshalEasyJSON(lexer *jlexer.Lexer) { // r := jreader.NewReaderFromEasyJSONLexer(lexer) // s.ReadFromJSONReader(&r) // } package jreader go-jsonstream-3.0.0/jreader/reader.go000066400000000000000000000314461430321663200175170ustar00rootroot00000000000000package jreader // Reader is a high-level API for reading JSON data sequentially. // // It is designed to make writing custom unmarshallers for application types as convenient as // possible. The general usage pattern is as follows: // // - Values are parsed in the order that they appear. // // - In general, the caller should know what data type is expected. Since it is common for // properties to be nullable, the methods for reading scalar types have variants for allowing // a null instead of the specified type. If the type is completely unknown, use Any. // // - For reading array or object structures, the Array and Object methods return a struct that // keeps track of additional reader state while that structure is being parsed. // // - If any method encounters an error (due to either malformed JSON, or well-formed JSON that // did not match the caller's data type expectations), the Reader permanently enters a failed // state and remembers that error; all subsequent method calls will return the same error and no // more parsing will happen. This means that the caller does not necessarily have to check the // error return value of any individual method, although it can. type Reader struct { tr tokenReader awaitingReadValue bool // used by ArrayState & ObjectState err error } // Error returns the first error that the Reader encountered, if the Reader is in a failed state, // or nil if it is still in a good state. func (r *Reader) Error() error { return r.err } // RequireEOF returns nil if all of the input has been consumed (not counting whitespace), or an // error if not. func (r *Reader) RequireEOF() error { if !r.tr.EOF() { return SyntaxError{Message: errMsgDataAfterEnd, Offset: r.tr.LastPos()} } return nil } // AddError sets the Reader's error value and puts it into a failed state. If the parameter is nil // or the Reader was already in a failed state, it does nothing. func (r *Reader) AddError(err error) { if r.err == nil { r.err = err } } // ReplaceError sets the Reader's error value and puts it into a failed state, replacing any // previously reported error. If the parameter is nil, it does nothing (a failed state cannot be // changed to a non-failed state). func (r *Reader) ReplaceError(err error) { if err != nil { r.err = err } } // Null attempts to read a null value, returning an error if the next token is not a null. func (r *Reader) Null() error { r.awaitingReadValue = false if r.err != nil { return r.err } isNull, err := r.tr.Null() if isNull || err != nil { return err } return r.typeErrorForCurrentToken(NullValue, false) } // Bool attempts to read a boolean value. // // If there is a parsing error, or the next value is not a boolean, the return value is false // and the Reader enters a failed state, which you can detect with Error(). func (r *Reader) Bool() bool { r.awaitingReadValue = false if r.err != nil { return false } val, err := r.tr.Bool() if err != nil { r.err = err return false } return val } // BoolOrNull attempts to read either a boolean value or a null. In the case of a boolean, the return // values are (value, true); for a null, they are (false, false). // // If there is a parsing error, or the next value is neither a boolean nor a null, the return values // are (false, false) and the Reader enters a failed state, which you can detect with Error(). func (r *Reader) BoolOrNull() (value bool, nonNull bool) { r.awaitingReadValue = false if r.err != nil { return false, false } isNull, err := r.tr.Null() if isNull || err != nil { r.err = err return false, false } val, err := r.tr.Bool() if err != nil { r.err = typeErrorForNullableValue(err) return false, false } return val, true } // Int attempts to read a numeric value and returns it as an int. // // If there is a parsing error, or the next value is not a number, the return value is zero and // the Reader enters a failed state, which you can detect with Error(). Non-numeric types are never // converted to numbers. func (r *Reader) Int() int { return int(r.Float64()) } // IntOrNull attempts to read either an integer numeric value or a null. In the case of a number, the // return values are (value, true); for a null, they are (0, false). // // If there is a parsing error, or the next value is neither a number nor a null, the return values // are (0, false) and the Reader enters a failed state, which you can detect with Error(). func (r *Reader) IntOrNull() (int, bool) { val, nonNull := r.Float64OrNull() return int(val), nonNull } // Float64 attempts to read a numeric value and returns it as a float64. // // If there is a parsing error, or the next value is not a number, the return value is zero and // the Reader enters a failed state, which you can detect with Error(). Non-numeric types are never // converted to numbers. func (r *Reader) Float64() float64 { r.awaitingReadValue = false if r.err != nil { return 0 } val, err := r.tr.Number() if err != nil { r.err = err return 0 } return val } // Float64OrNull attempts to read either a numeric value or a null. In the case of a number, the // return values are (value, true); for a null, they are (0, false). // // If there is a parsing error, or the next value is neither a number nor a null, the return values // are (0, false) and the Reader enters a failed state, which you can detect with Error(). func (r *Reader) Float64OrNull() (float64, bool) { r.awaitingReadValue = false if r.err != nil { return 0, false } isNull, err := r.tr.Null() if isNull || err != nil { r.err = err return 0, false } val, err := r.tr.Number() if err != nil { r.err = typeErrorForNullableValue(err) return 0, false } return val, true } // String attempts to read a string value. // // If there is a parsing error, or the next value is not a string, the return value is "" and // the Reader enters a failed state, which you can detect with Error(). Types other than string // are never converted to strings. func (r *Reader) String() string { r.awaitingReadValue = false if r.err != nil { return "" } val, err := r.tr.String() if err != nil { r.err = err return "" } return val } // StringOrNull attempts to read either a string value or a null. In the case of a string, the // return values are (value, true); for a null, they are ("", false). // // If there is a parsing error, or the next value is neither a string nor a null, the return values // are ("", false) and the Reader enters a failed state, which you can detect with Error(). func (r *Reader) StringOrNull() (string, bool) { r.awaitingReadValue = false if r.err != nil { return "", false } isNull, err := r.tr.Null() if isNull || err != nil { r.err = err return "", false } val, err := r.tr.String() if err != nil { r.err = typeErrorForNullableValue(err) return "", false } return val, true } // Array attempts to begin reading a JSON array value. If successful, the return value will be an // ArrayState containing the necessary state for iterating through the array elements. // // The ArrayState is used only for the iteration state; to read the value of each array element, you // will still use the Reader's methods. // // If there is a parsing error, or the next value is not an array, the returned ArrayState is a stub // whose Next() method always returns false, and the Reader enters a failed state, which you can // detect with Error(). // // See ArrayState for example code. func (r *Reader) Array() ArrayState { return r.tryArray(false) } // ArrayOrNull attempts to either begin reading an JSON array value, or read a null. In the case of an // array, the return value will be an ArrayState containing the necessary state for iterating through // the array elements; the ArrayState's IsDefined() method will return true. In the case of a null, the // returned ArrayState will be a stub whose Next() and IsDefined() methods always returns false. // // The ArrayState is used only for the iteration state; to read the value of each array element, you // will still use the Reader's methods. // // If there is a parsing error, or the next value is neither an array nor a null, the return value is // the same as for a null but the Reader enters a failed state, which you can detect with Error(). // // See ArrayState for example code. func (r *Reader) ArrayOrNull() ArrayState { return r.tryArray(true) } func (r *Reader) tryArray(allowNull bool) ArrayState { r.awaitingReadValue = false if r.err != nil { return ArrayState{} } if allowNull { isNull, err := r.tr.Null() if err != nil { r.err = err return ArrayState{} } if isNull { return ArrayState{} } } gotDelim, err := r.tr.Delimiter('[') if err != nil { r.err = err return ArrayState{} } if gotDelim { return ArrayState{r: r} } r.err = r.typeErrorForCurrentToken(ArrayValue, allowNull) return ArrayState{} } // Object attempts to begin reading a JSON object value. If successful, the return value will be an // ObjectState containing the necessary state for iterating through the object properties. // // The ObjectState is used only for the iteration state; to read the value of each property, you // will still use the Reader's methods. // // If there is a parsing error, or the next value is not an object, the returned ObjectState is a stub // whose Next() method always returns false, and the Reader enters a failed state, which you can // detect with Error(). // // See ObjectState for example code. func (r *Reader) Object() ObjectState { return r.tryObject(false) } // ObjectOrNull attempts to either begin reading an JSON object value, or read a null. In the case of an // object, the return value will be an ObjectState containing the necessary state for iterating through // the object properties; the ObjectState's IsDefined() method will return true. In the case of a null, // the returned ObjectState will be a stub whose Next() and IsDefined() methods always returns false. // // The ObjectState is used only for the iteration state; to read the value of each property, you // will still use the Reader's methods. // // If there is a parsing error, or the next value is neither an object nor a null, the return value is // the same as for a null but the Reader enters a failed state, which you can detect with Error(). // // See ObjectState for example code. func (r *Reader) ObjectOrNull() ObjectState { return r.tryObject(true) } func (r *Reader) tryObject(allowNull bool) ObjectState { r.awaitingReadValue = false if r.err != nil { return ObjectState{} } if allowNull { isNull, err := r.tr.Null() if err != nil || isNull { r.err = err return ObjectState{} } } gotDelim, err := r.tr.Delimiter('{') if err != nil { r.err = err return ObjectState{} } if gotDelim { return ObjectState{r: r} } r.err = r.typeErrorForCurrentToken(ObjectValue, allowNull) return ObjectState{} } // Any reads a single value of any type, if it is a scalar value or a null, or prepares to read // the value if it is an array or object. // // The returned AnyValue's Kind field indicates the value type. If it is BoolValue, NumberValue, // or StringValue, check the corresponding Bool, Number, or String property. If it is ArrayValue // or ObjectValue, the AnyValue's Array or Object field has been initialized with an ArrayState or // ObjectState just as if you had called the Reader's Array or Object method. // // If there is a parsing error, the return value is the same as for a null and the Reader enters // a failed state, which you can detect with Error(). func (r *Reader) Any() AnyValue { r.awaitingReadValue = false if r.err != nil { return AnyValue{} } v, err := r.tr.Any() if err != nil { r.err = err return AnyValue{} } switch v.Kind { case BoolValue: return AnyValue{Kind: v.Kind, Bool: v.Bool} case NumberValue: return AnyValue{Kind: v.Kind, Number: v.Number} case StringValue: return AnyValue{Kind: v.Kind, String: v.String} case ArrayValue: return AnyValue{Kind: v.Kind, Array: ArrayState{r: r}} case ObjectValue: return AnyValue{Kind: v.Kind, Object: ObjectState{r: r}} default: return AnyValue{Kind: NullValue} } } // SkipValue consumes and discards the next JSON value of any type. For an array or object value, it // recurses to also consume and discard all array elements or object properties. func (r *Reader) SkipValue() error { r.awaitingReadValue = false if r.err != nil { return r.err } v := r.Any() if v.Kind == ArrayValue { for v.Array.Next() { } } else if v.Kind == ObjectValue { for v.Object.Next() { } } return r.err } func typeErrorForNullableValue(err error) error { if err != nil { switch e := err.(type) { //nolint:gocritic case TypeError: e.Nullable = true return e } } return err } func (r *Reader) typeErrorForCurrentToken(expected ValueKind, nullable bool) error { v, err := r.tr.Any() if err != nil { return err } return TypeError{Expected: expected, Actual: v.Kind, Offset: r.tr.LastPos(), Nullable: nullable} } go-jsonstream-3.0.0/jreader/reader_array.go000066400000000000000000000042671430321663200207160ustar00rootroot00000000000000package jreader // ArrayState is returned by Reader's Array and ArrayOrNull methods. Use it in conjunction with // Reader to iterate through a JSON array. To read the value of each array element, you will still // use the Reader's methods. // // This example reads an array of strings; if there is a null instead of an array, it behaves the // same as for an empty array. Note that it is not necessary to check for an error result before // iterating over the ArrayState, or to break out of the loop if String causes an error, because // the ArrayState's Next method will return false if the Reader has had any errors. // // var values []string // for arr := r.ArrayOrNull(); arr.Next(); { // if s := r.String(); r.Error() == nil { // values = append(values, s) // } // } type ArrayState struct { r *Reader afterFirst bool } // IsDefined returns true if the ArrayState represents an actual array, or false if it was // parsed from a null value or was the result of an error. If IsDefined is false, Next will // always return false. The zero value ArrayState{} returns false for IsDefined. func (arr *ArrayState) IsDefined() bool { return arr.r != nil } // Next checks whether an array element is available and returns true if so. It returns false // if the Reader has reached the end of the array, or if any previous Reader operation failed, // or if the array was empty or null. // // If Next returns true, you can then use Reader methods such as Bool or String to read the // element value. If you do not care about the value, simply calling Next again without calling // a Reader method will discard the value, just as if you had called SkipValue on the reader. // // See ArrayState for example code. func (arr *ArrayState) Next() bool { if arr.r == nil || arr.r.err != nil { return false } var isEnd bool var err error if arr.afterFirst { if arr.r.awaitingReadValue { if err := arr.r.SkipValue(); err != nil { return false } } isEnd, err = arr.r.tr.EndDelimiterOrComma(']') } else { arr.afterFirst = true isEnd, err = arr.r.tr.Delimiter(']') } if err != nil { arr.r.AddError(err) return false } if !isEnd { arr.r.awaitingReadValue = true } return !isEnd } go-jsonstream-3.0.0/jreader/reader_array_test.go000066400000000000000000000011651430321663200217470ustar00rootroot00000000000000package jreader import ( "errors" "testing" "github.com/stretchr/testify/require" ) func TestAddErrorStopsArrayParsing(t *testing.T) { r := NewReader([]byte("[1,2]")) arr := r.Array() require.True(t, arr.Next()) require.Equal(t, 1, r.Int()) err := errors.New("sorry") r.AddError(err) require.Equal(t, err, r.Error()) require.False(t, arr.Next()) require.Equal(t, 0, r.Int()) require.Equal(t, err, r.Error()) } func TestSyntaxErrorStopsArrayParsing(t *testing.T) { r := NewReader([]byte("[bad,1,2]")) arr := r.Array() require.False(t, arr.Next()) require.Equal(t, 0, r.Int()) require.Error(t, r.Error()) } go-jsonstream-3.0.0/jreader/reader_benchmark_test.go000066400000000000000000000077471430321663200225770ustar00rootroot00000000000000package jreader import ( "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" ) func BenchmarkReadNullNoAlloc(b *testing.B) { data := []byte("null") b.ResetTimer() for i := 0; i < b.N; i++ { r := NewReader(data) if err := r.Null(); err != nil { b.FailNow() } } } func BenchmarkReadBooleanNoAlloc(b *testing.B) { data := []byte("true") b.ResetTimer() for i := 0; i < b.N; i++ { r := NewReader(data) val := r.Bool() if !val || r.Error() != nil { b.FailNow() } } } func BenchmarkReadNumberIntNoAlloc(b *testing.B) { data := []byte("1234") b.ResetTimer() for i := 0; i < b.N; i++ { r := NewReader(data) val := r.Int() failBenchmarkOnReaderError(b, &r) if val != 1234 { b.FailNow() } } } func BenchmarkReadNumberFloat(b *testing.B) { data := []byte("1234.5") b.ResetTimer() for i := 0; i < b.N; i++ { r := NewReader(data) val := r.Float64() failBenchmarkOnReaderError(b, &r) if val != 1234.5 { b.FailNow() } } } func BenchmarkReadString(b *testing.B) { data := []byte(`"abc"`) b.ResetTimer() for i := 0; i < b.N; i++ { r := NewReader(data) val := r.String() failBenchmarkOnReaderError(b, &r) if val != "abc" { b.FailNow() } } } func BenchmarkReadArrayOfBools(b *testing.B) { expected := commontest.MakeBools() data := commontest.MakeBoolsJSON(expected) b.ResetTimer() for i := 0; i < b.N; i++ { var vals []bool r := NewReader(data) arr := r.Array() failBenchmarkOnReaderError(b, &r) for arr.Next() { val := r.Bool() failBenchmarkOnReaderError(b, &r) vals = append(vals, val) } if len(vals) < len(expected) { b.FailNow() } } } func BenchmarkReadArrayOfStrings(b *testing.B) { expected := commontest.MakeStrings() data := commontest.MakeStringsJSON(expected) b.ResetTimer() for i := 0; i < b.N; i++ { var vals []string r := NewReader(data) arr := r.Array() failBenchmarkOnReaderError(b, &r) for arr.Next() { val := r.String() failBenchmarkOnReaderError(b, &r) vals = append(vals, val) } if len(vals) < len(expected) { b.FailNow() } } } func BenchmarkReadArrayOfNullsNoAlloc(b *testing.B) { // This just verifies that simply parsing an array doesn't cause any allocations, if the values don't. data := []byte(`[null,null]`) b.ResetTimer() for i := 0; i < b.N; i++ { r := NewReader(data) arr := r.Array() failBenchmarkOnReaderError(b, &r) if !arr.Next() { b.FailNow() } if err := r.Null(); err != nil { b.FailNow() } if !arr.Next() { b.FailNow() } failBenchmarkOnReaderError(b, &r) if arr.Next() { b.FailNow() } } } func BenchmarkReadObjectNoAlloc(b *testing.B) { for i := 0; i < b.N; i++ { var val ExampleStructWrapper r := NewReader(commontest.ExampleStructData) val.ReadFromJSONReader(&r) failBenchmarkOnReaderError(b, &r) if val != ExampleStructWrapper(commontest.ExampleStructValue) { b.FailNow() } } } func BenchmarkReadArrayOfObjects(b *testing.B) { rawStructs := commontest.MakeStructs() data := commontest.MakeStructsJSON(rawStructs) var expected []ExampleStructWrapper for _, rawStruct := range rawStructs { expected = append(expected, ExampleStructWrapper(rawStruct)) } b.ResetTimer() for i := 0; i < b.N; i++ { values := make([]ExampleStructWrapper, 0) r := NewReader(data) for arr := r.Array(); arr.Next(); { var val ExampleStructWrapper val.ReadFromJSONReader(&r) values = append(values, val) } failBenchmarkOnReaderError(b, &r) for i, val := range values { if val != expected[i] { b.FailNow() } } } } func BenchmarkReadObjectWithRequiredPropsNoAlloc(b *testing.B) { for i := 0; i < b.N; i++ { var val ExampleStructWrapperWithRequiredProps r := NewReader(commontest.ExampleStructData) val.ReadFromJSONReader(&r) failBenchmarkOnReaderError(b, &r) if val != ExampleStructWrapperWithRequiredProps(commontest.ExampleStructValue) { b.FailNow() } } } func failBenchmarkOnReaderError(b *testing.B, r *Reader) { if r.Error() != nil { b.Error(r.Error()) b.FailNow() } } go-jsonstream-3.0.0/jreader/reader_examples_test.go000066400000000000000000000134501430321663200224470ustar00rootroot00000000000000package jreader import "fmt" func ExampleNewReader() { r := NewReader([]byte(`"a \"good\" string"`)) s := r.String() if err := r.Error(); err != nil { fmt.Println("error:", err.Error()) } else { fmt.Println(s) } // Output: a "good" string } func ExampleReader_RequireEOF() { r := NewReader([]byte(`100,"extra"`)) n := r.Int() err := r.RequireEOF() fmt.Println(n, err) // Output: 100 unexpected data after end of JSON value at position 3 } func ExampleReader_AddError() { r := NewReader([]byte(`[1,2,3,4,5]`)) values := []int{} for arr := r.Array(); arr.Next(); { n := r.Int() values = append(values, n) if n > 1 { r.AddError(fmt.Errorf("got an error after %d", n)) } } err := r.Error() fmt.Println(values, err) // Output: [1 2] got an error after 2 } func ExampleReader_Null() { r := NewReader([]byte(`null`)) if err := r.Null(); err != nil { fmt.Println("error:", err) } else { fmt.Println("got a null") } // Output: got a null } func ExampleReader_Bool() { r := NewReader([]byte(`true`)) var value bool = r.Bool() if err := r.Error(); err != nil { fmt.Println("error:", err) } else { fmt.Println("value:", value) } // Output: value: true } func ExampleReader_BoolOrNull() { r1 := NewReader([]byte(`null`)) if value1, nonNull := r1.BoolOrNull(); nonNull { fmt.Println("value1:", value1) } r2 := NewReader([]byte(`false`)) if value2, nonNull := r2.BoolOrNull(); nonNull { fmt.Println("value2:", value2) } // Output: value2: false } func ExampleReader_Int() { r := NewReader([]byte(`123`)) var value int = r.Int() if err := r.Error(); err != nil { fmt.Println("error:", err) } else { fmt.Println("value:", value) } // Output: value: 123 } func ExampleReader_IntOrNull() { r1 := NewReader([]byte(`null`)) if value1, nonNull := r1.IntOrNull(); nonNull { fmt.Println("value1:", value1) } r2 := NewReader([]byte(`0`)) if value2, nonNull := r2.IntOrNull(); nonNull { fmt.Println("value2:", value2) } // Output: value2: 0 } func ExampleReader_Float64() { r := NewReader([]byte(`1234.5`)) var value float64 = r.Float64() if err := r.Error(); err != nil { fmt.Println("error:", err) } else { fmt.Println("value:", value) } // Output: value: 1234.5 } func ExampleReader_Float64OrNull() { r1 := NewReader([]byte(`null`)) if value1, nonNull := r1.Float64OrNull(); nonNull { fmt.Println("value1:", value1) } r2 := NewReader([]byte(`0`)) if value2, nonNull := r2.Float64OrNull(); nonNull { fmt.Println("value2:", value2) } // Output: value2: 0 } func ExampleReader_String() { r := NewReader([]byte(`"a \"good\" string"`)) var value string = r.String() if err := r.Error(); err != nil { fmt.Println("error:", err) } else { fmt.Println("value:", value) } // Output: value: a "good" string } func ExampleReader_StringOrNull() { r1 := NewReader([]byte(`null`)) if value1, nonNull := r1.StringOrNull(); nonNull { fmt.Println("value1:", "\""+value1+"\"") } r2 := NewReader([]byte(`""`)) if value2, nonNull := r2.StringOrNull(); nonNull { fmt.Println("value2:", "\""+value2+"\"") } // Output: value2: "" } func ExampleReader_Array() { r := NewReader([]byte(`[1,2]`)) values := []int{} for arr := r.Array(); arr.Next(); { values = append(values, r.Int()) } fmt.Println("values:", values) // Output: values: [1 2] } func ExampleReader_ArrayOrNull() { printArray := func(input string) { r := NewReader([]byte(input)) values := []int{} arr := r.Array() for arr.Next() { values = append(values, r.Int()) } fmt.Println(input, "->", values, "... IsDefined =", arr.IsDefined()) } printArray("null") printArray("[1,2]") // Output: null -> [] ... IsDefined = false // [1,2] -> [1 2] ... IsDefined = true } func ExampleReader_Object() { r := NewReader([]byte(`{"a":1,"b":2}`)) items := []string{} for obj := r.Object(); obj.Next(); { name := obj.Name() value := r.Int() items = append(items, fmt.Sprintf("%s=%d", name, value)) } fmt.Println("items:", items) // Output: items: [a=1 b=2] } func ExampleReader_ObjectOrNull() { printObject := func(input string) { r := NewReader([]byte(input)) items := []string{} obj := r.Object() for obj.Next() { name := obj.Name() value := r.Int() items = append(items, fmt.Sprintf("%s=%d", name, value)) } fmt.Println(input, "->", items, "... IsDefined =", obj.IsDefined()) } printObject("null") printObject(`{"a":1,"b":2}`) // Output: null -> [] ... IsDefined = false // {"a":1,"b":2} -> [a=1 b=2] ... IsDefined = true } func ExampleReader_Any() { printValue := func(input string) { r := NewReader([]byte(input)) value := r.Any() switch value.Kind { case NullValue: fmt.Println("a null") case BoolValue: fmt.Println("a bool:", value.Bool) case NumberValue: fmt.Println("a number:", value.Number) case StringValue: fmt.Println("a string:", value.String) case ArrayValue: n := 0 for value.Array.Next() { n++ // for this example, we're not looking at the actual element value } fmt.Println("an array with", n, "elements") case ObjectValue: n := 0 for value.Object.Next() { n++ // for this example, we're not looking at the actual element value } } } printValue(`123`) printValue(`["a","b"]`) // Output: a number: 123 // an array with 2 elements } func ExampleObjectState_WithRequiredProperties() { requiredProps := []string{"key", "name"} r := NewReader([]byte(`{"name": "x"}`)) var key, name string for obj := r.Object().WithRequiredProperties(requiredProps); obj.Next(); { switch string(obj.Name()) { case "key": key = r.String() case "name": name = r.String() } } if err := r.Error(); err != nil { if rpe, ok := err.(RequiredPropertyError); ok { fmt.Println("missing property:", rpe.Name) } else { fmt.Println("unexpected error:", err) } } else { fmt.Println(key, name) } // Output: missing property: key } go-jsonstream-3.0.0/jreader/reader_init_default.go000066400000000000000000000006411430321663200222370ustar00rootroot00000000000000package jreader // NewReader creates a Reader that consumes the specified JSON input data. // // This function returns the struct by value (Reader, not *Reader). This avoids the overhead of a // heap allocation since, in typical usage, the Reader will not escape the scope in which it was // declared and can remain on the stack. func NewReader(data []byte) Reader { return Reader{ tr: newTokenReader(data), } } go-jsonstream-3.0.0/jreader/reader_init_easyjson.go000066400000000000000000000015621430321663200224510ustar00rootroot00000000000000//go:build launchdarkly_easyjson // +build launchdarkly_easyjson package jreader import ( "github.com/mailru/easyjson/jlexer" ) // NewReaderFromEasyJSONLexer creates a Reader that consumes JSON input data from the specified easyjson // jlexer.Lexer. // // This function is only available in code that was compiled with the build tag "launchdarkly_easyjson". // Its purpose is to allow custom unmarshaling code that is based on the Reader API to be used as // efficiently as possible within other data structures that are being unmarshaled with easyjson. // Directly using the same Lexer that is already being used is more efficient than asking Lexer to // scan the next object, return it as a byte slice, and then pass that byte slice to NewReader. func NewReaderFromEasyJSONLexer(lexer *jlexer.Lexer) Reader { return Reader{ tr: newTokenReaderFromEasyjsonLexer(lexer), } } go-jsonstream-3.0.0/jreader/reader_init_easyjson_test.go000066400000000000000000000023631430321663200235100ustar00rootroot00000000000000//go:build launchdarkly_easyjson // +build launchdarkly_easyjson package jreader import ( "testing" "github.com/mailru/easyjson/jlexer" "github.com/stretchr/testify/require" ) func TestNewReaderFromEasyJSONLexer(t *testing.T) { data := []byte(`[1,{"property":2},3]`) lexer := jlexer.Lexer{Data: data} // Parse the first part of the JSON array directly with the Lexer lexer.Delim('[') require.NoError(t, lexer.Error()) n := lexer.Int() require.Equal(t, 1, n) require.NoError(t, lexer.Error()) lexer.WantComma() // Now pick up where we left off and use the Reader to parse {"property":2} reader := NewReaderFromEasyJSONLexer(&lexer) obj := reader.Object() require.NoError(t, reader.Error()) require.True(t, obj.Next()) require.Equal(t, "property", string(obj.Name())) n = reader.Int() require.NoError(t, reader.Error()) require.Equal(t, 2, n) require.False(t, obj.Next()) // The Lexer should be left in the proper state to parse the rest of the stream require.NoError(t, lexer.Error()) lexer.WantComma() n = lexer.Int() require.Equal(t, 3, n) require.NoError(t, lexer.Error()) lexer.WantComma() // we don't actually want a comma here, but that's how you read arrays in easyjson lexer.Delim(']') require.NoError(t, lexer.Error()) } go-jsonstream-3.0.0/jreader/reader_object.go000066400000000000000000000144501430321663200210410ustar00rootroot00000000000000package jreader // ObjectState is returned by Reader's Object and ObjectOrNull methods. Use it in conjunction with // Reader to iterate through a JSON object. To read the value of each object property, you will // still use the Reader's methods. Properties may appear in any order. // // This example reads an object whose values are strings; if there is a null instead of an object, // it behaves the same as for an empty object. Note that it is not necessary to check for an error // result before iterating over the ObjectState, or to break out of the loop if String causes an // error, because the ObjectState's Next method will return false if the Reader has had any errors. // // values := map[string]string // for obj := r.ObjectOrNull(); obj.Next(); { // key := string(obj.Name()) // if s := r.String(); r.Error() == nil { // values[key] = s // } // } // // The next example reads an object with two expected property names, "a" and "b". Any unrecognized // properties are ignored. // // var result struct { // a int // b int // } // for obj := r.ObjectOrNull(); obj.Next(); { // switch string(obj.Name()) { // case "a": // result.a = r.Int() // case "b": // result.b = r.Int() // } // } // // If the schema requires certain properties to always be present, the WithRequiredProperties method is // a convenient way to enforce this. type ObjectState struct { r *Reader afterFirst bool name []byte requiredProps []string requiredPropsFound []bool requiredPropsPrealloc [20]bool // used as initial base array for requiredPropsFound to avoid allocation } // WithRequiredProperties adds a requirement that the specified JSON property name(s) must appear // in the JSON object at some point before it ends. // // This method returns a new, modified ObjectState. It should be called before the first time you // call Next. For instance: // // requiredProps := []string{"key", "name"} // for obj := reader.Object().WithRequiredProperties(requiredProps); obj.Next(); { // switch string(obj.Name()) { ... } // } // // When the end of the object is reached (and Next() returns false), if one of the required // properties has not yet been seen, and no other error has occurred, the Reader's error state // will be set to a RequiredPropertyError. // // For efficiency, it is best to preallocate the list of property names globally rather than creating // it inline. func (obj ObjectState) WithRequiredProperties(requiredProps []string) ObjectState { ret := obj if len(requiredProps) > 0 { ret.requiredProps = requiredProps } return ret } // IsDefined returns true if the ObjectState represents an actual object, or false if it was // parsed from a null value or was the result of an error. If IsDefined is false, Next will // always return false. The zero value ObjectState{} returns false for IsDefined. func (obj *ObjectState) IsDefined() bool { return obj.r != nil } // Next checks whether an object property is available and returns true if so. It returns false // if the Reader has reached the end of the object, or if any previous Reader operation failed, // or if the object was empty or null. // // If Next returns true, you can then get the property name with Name, and use Reader methods // such as Bool or String to read the property value. If you do not care about the value, simply // calling Next again without calling a Reader method will discard the value, just as if you had // called SkipValue on the reader. // // See ObjectState for example code. func (obj *ObjectState) Next() bool { if obj.r == nil || obj.r.err != nil { return false } var isEnd bool var err error if !obj.afterFirst && len(obj.requiredProps) != 0 { // Initialize the bool slice that we'll use to keep track of what properties we found. // See comment on requiredPropsFoundSlice(). if len(obj.requiredProps) > len(obj.requiredPropsPrealloc) { obj.requiredPropsFound = make([]bool, len(obj.requiredProps)) } } if obj.afterFirst { if obj.r.awaitingReadValue { if err := obj.r.SkipValue(); err != nil { return false } } isEnd, err = obj.r.tr.EndDelimiterOrComma('}') } else { obj.afterFirst = true isEnd, err = obj.r.tr.Delimiter('}') } if err != nil { obj.r.AddError(err) return false } if isEnd { obj.name = nil if obj.requiredProps != nil { found := obj.requiredPropsFoundSlice() for i, requiredName := range obj.requiredProps { if !found[i] { obj.r.AddError(RequiredPropertyError{Name: requiredName, Offset: obj.r.tr.LastPos()}) break } } } return false } name, err := obj.r.tr.PropertyName() if err != nil { obj.r.AddError(err) return false } obj.name = name obj.r.awaitingReadValue = true if obj.requiredProps != nil { found := obj.requiredPropsFoundSlice() for i, requiredName := range obj.requiredProps { if requiredName == string(name) { found[i] = true break } } } return true } // Name returns the name of the current object property, or nil if there is no current property // (that is, if Next returned false or if Next was never called). // // For efficiency, to avoid allocating a string for each property name, the name is returned as a // byte slice which may refer directly to the source data. Casting this to a string within a simple // comparison expression or switch statement should not cause a string allocation; the Go compiler // optimizes these into direct byte-slice comparisons. func (obj *ObjectState) Name() []byte { return obj.name } // This technique of using either a preallocated fixed-length array or a slice (where we have // only set the slice to a non-nil value if we determined that the array wasn't big enough) is a // way to avoid unnecessary heap allocations: if the ObjectState is on the stack, the fixed-length // array can stay on the stack too. In order for this to work, we *cannot* set the slice to refer // to the array (obj.requiredProps = obj.requiredPropsFound[0:len(obj.requiredProps)]); the Go // compiler can't prove that that's safe, so it will make everything escape to the heap. Instead // we have to conditionally reference one or the other here. func (obj *ObjectState) requiredPropsFoundSlice() []bool { if obj.requiredPropsFound != nil { return obj.requiredPropsFound } return obj.requiredPropsPrealloc[0:len(obj.requiredProps)] } go-jsonstream-3.0.0/jreader/reader_object_test.go000066400000000000000000000030211430321663200220700ustar00rootroot00000000000000package jreader import ( "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAddErrorStopsObjectParsing(t *testing.T) { r := NewReader([]byte(`{"a":1, "b":2}`)) obj := r.Object() require.True(t, obj.Next()) require.Equal(t, "a", string(obj.Name())) require.Equal(t, 1, r.Int()) err := errors.New("sorry") r.AddError(err) require.Equal(t, err, r.Error()) require.False(t, obj.Next()) require.Equal(t, err, r.Error()) } func TestSyntaxErrorStopsObjectParsing(t *testing.T) { r := NewReader([]byte(`{"a":1, x: 2, "c":3}`)) obj := r.Object() require.True(t, obj.Next()) require.Equal(t, "a", string(obj.Name())) require.Equal(t, 1, r.Int()) require.False(t, obj.Next()) require.Equal(t, 0, r.Int()) require.Error(t, r.Error()) require.False(t, obj.Next()) } func TestRequiredPropertiesAreAllFound(t *testing.T) { data := []byte(`{"a":1, "b":2, "c":3}`) requiredProps := []string{"c", "b", "a"} r := NewReader(data) for obj := r.Object().WithRequiredProperties(requiredProps); obj.Next(); { } require.NoError(t, r.Error()) } func TestRequiredPropertyIsNotFound(t *testing.T) { data := []byte(`{"a":1, "c":3}`) requiredProps := []string{"c", "b", "a"} r := NewReader(data) for obj := r.Object().WithRequiredProperties(requiredProps); obj.Next(); { } require.Error(t, r.Error()) require.IsType(t, RequiredPropertyError{}, r.Error()) rpe := r.Error().(RequiredPropertyError) assert.Equal(t, "b", rpe.Name) assert.GreaterOrEqual(t, rpe.Offset, len(data)-1) } go-jsonstream-3.0.0/jreader/reader_test.go000066400000000000000000000316571430321663200205620ustar00rootroot00000000000000package jreader import ( "errors" "fmt" "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" "github.com/stretchr/testify/require" ) // This uses the framework defined in the commontest package to exercise Reader with a large number // of valid and invalid JSON inputs. All we need to implement here is the logic for calling the // appropriate Reader methods that correspond to the commontest abstractions. type readerTestContext struct { input []byte r *Reader } type readerValueTestFactory struct{} type readerErrorTestFactory struct{} // The behavior of Reader is flexible so that callers can choose to read the same JSON value in // several different ways. Therefore, we generate variants for each value test as follows: // - A null JSON value could be read either as a null, or as a nullable value of another type. // - A JSON number could be read as an int (if the test value is an int), a float, or a nullable // int or float. // - Any other non-null value could be read as its own type, or as a nullable value of that type. // - Any value could be read with a nonspecific type using the Any() method. // - Any value could be skipped instead of read. const ( defaultVariant commontest.ValueVariant = "" nullableValue commontest.ValueVariant = "nullable" numberAsInt commontest.ValueVariant = "int" nullableNumberAsInt commontest.ValueVariant = "nullable int" nullableBoolIsNull commontest.ValueVariant = "nullable bool is" nullableIntIsNull commontest.ValueVariant = "nullable int is" nullableFloatIsNull commontest.ValueVariant = "nullable float is" nullableStringIsNull commontest.ValueVariant = "nullable string is" nullableArrayIsNull commontest.ValueVariant = "nullable array is" nullableObjectIsNull commontest.ValueVariant = "nullable object is" ) var variantsForNullValues = []commontest.ValueVariant{defaultVariant, nullableBoolIsNull, nullableIntIsNull, nullableFloatIsNull, nullableStringIsNull, nullableArrayIsNull, nullableObjectIsNull, commontest.UntypedVariant, commontest.SkipValueVariant} var variantsForInts = []commontest.ValueVariant{defaultVariant, numberAsInt, nullableValue, nullableNumberAsInt, commontest.UntypedVariant, commontest.SkipValueVariant} var variantsForFloats = []commontest.ValueVariant{defaultVariant, nullableValue, commontest.UntypedVariant, commontest.SkipValueVariant} var variantsForNonNullValues = []commontest.ValueVariant{defaultVariant, nullableValue, commontest.UntypedVariant, commontest.SkipValueVariant} var shouldHaveBeenNullError = errors.New("should have been null") var shouldNotHaveBeenNullError = errors.New("should not have been null") func TestReader(t *testing.T) { ts := commontest.ReaderTestSuite{ ContextFactory: func(input []byte) commontest.TestContext { r := NewReader(input) return &readerTestContext{input: input, r: &r} }, ValueTestFactory: readerValueTestFactory{}, ReadErrorTestFactory: readerErrorTestFactory{}, } ts.Run(t) } func (c readerTestContext) JSONData() []byte { return c.input } func (f readerValueTestFactory) EOF() commontest.Action { return func(c commontest.TestContext) error { return c.(*readerTestContext).r.RequireEOF() } } func (f readerValueTestFactory) Variants(value commontest.AnyValue) []commontest.ValueVariant { switch value.Kind { case commontest.NullValue: return variantsForNullValues case commontest.NumberValue: if float64(int(value.Number)) == value.Number { return variantsForInts } return variantsForFloats default: return variantsForNonNullValues } } func (f readerValueTestFactory) Value(value commontest.AnyValue, variant commontest.ValueVariant) commontest.Action { return func(c commontest.TestContext) error { ctx := c.(*readerTestContext) r := ctx.r if variant == commontest.SkipValueVariant { return r.SkipValue() } if variant == commontest.UntypedVariant { return assertReadAnyValue(ctx, r, value) } switch value.Kind { case commontest.NullValue: return assertReadNull(r, variant) case commontest.BoolValue: switch variant { case nullableValue: gotVal, nonNull := r.BoolOrNull() return commontest.AssertNoErrors(r.Error(), commontest.AssertTrue(nonNull, shouldNotHaveBeenNullError.Error()), commontest.AssertEqual(value.Bool, gotVal)) default: gotVal := r.Bool() return commontest.AssertNoErrors(r.Error(), commontest.AssertEqual(value.Bool, gotVal)) } case commontest.NumberValue: switch variant { case nullableNumberAsInt: gotVal, nonNull := r.IntOrNull() return commontest.AssertNoErrors(r.Error(), commontest.AssertTrue(nonNull, shouldNotHaveBeenNullError.Error()), commontest.AssertEqual(int(value.Number), gotVal)) case numberAsInt: gotVal := r.Int() return commontest.AssertNoErrors(r.Error(), commontest.AssertEqual(int(value.Number), gotVal)) case nullableValue: gotVal, nonNull := r.Float64OrNull() return commontest.AssertNoErrors(r.Error(), commontest.AssertTrue(nonNull, shouldNotHaveBeenNullError.Error()), commontest.AssertEqual(value.Number, gotVal)) default: gotVal := r.Float64() return commontest.AssertNoErrors(r.Error(), commontest.AssertEqual(value.Number, gotVal)) } case commontest.StringValue: switch variant { case nullableValue: gotVal, nonNull := r.StringOrNull() return commontest.AssertNoErrors(r.Error(), commontest.AssertTrue(nonNull, shouldNotHaveBeenNullError.Error()), commontest.AssertEqual(value.String, gotVal)) default: gotVal := r.String() return commontest.AssertNoErrors(r.Error(), commontest.AssertEqual(value.String, gotVal)) } case commontest.ArrayValue: var arr ArrayState if variant == nullableValue { arr = r.ArrayOrNull() } else { arr = r.Array() } if r.Error() != nil { return r.Error() } if err := commontest.AssertTrue(arr.IsDefined(), shouldNotHaveBeenNullError.Error()); err != nil { return err } return assertReadArray(ctx, &arr, value) case commontest.ObjectValue: var obj ObjectState if variant == nullableValue { obj = r.ObjectOrNull() } else { obj = r.Object() } if r.Error() != nil { return r.Error() } if err := commontest.AssertTrue(obj.IsDefined(), shouldNotHaveBeenNullError.Error()); err != nil { return err } return assertReadObject(ctx, &obj, value) } return nil } } func assertReadNull(r *Reader, variant commontest.ValueVariant) error { var gotVal, expectVal interface{} var nonNull bool switch variant { case defaultVariant: return r.Null() case nullableBoolIsNull: gotVal, nonNull = r.BoolOrNull() expectVal = false case nullableIntIsNull: gotVal, nonNull = r.IntOrNull() expectVal = 0 case nullableFloatIsNull: gotVal, nonNull = r.Float64OrNull() expectVal = float64(0) case nullableStringIsNull: gotVal, nonNull = r.StringOrNull() expectVal = "" case nullableArrayIsNull: arr := r.ArrayOrNull() if r.Error() != nil { return r.Error() } if arr.IsDefined() { return TypeError{Expected: NullValue, Actual: ArrayValue} } return nil case nullableObjectIsNull: obj := r.ObjectOrNull() if r.Error() != nil { return r.Error() } if obj.IsDefined() { return TypeError{Expected: NullValue, Actual: ObjectValue} } return nil } return commontest.AssertNoErrors( r.Error(), commontest.AssertTrue(!nonNull, shouldHaveBeenNullError.Error()), commontest.AssertEqual(expectVal, gotVal)) } func assertReadArray(ctx *readerTestContext, arr *ArrayState, value commontest.AnyValue) error { if err := commontest.AssertTrue(arr.IsDefined(), shouldNotHaveBeenNullError.Error()); err != nil { return err } for _, e := range value.Array { if err := commontest.AssertTrue(arr.Next(), "array ended too soon"); err != nil { return err } if err := e(ctx); err != nil { return err } } return commontest.AssertTrue(!arr.Next(), "expected end of array") } func assertReadObject(ctx *readerTestContext, obj *ObjectState, value commontest.AnyValue) error { if err := commontest.AssertTrue(obj.IsDefined(), "should not have been null"); err != nil { return err } for _, p := range value.Object { if err := commontest.AssertNoErrors( commontest.AssertTrue(obj.Next(), "object ended too soon"), commontest.AssertEqual(p.Name, string(obj.Name())), ); err != nil { return err } if err := p.Action(ctx); err != nil { return err } } return commontest.AssertTrue(!obj.Next(), "expected end of object") } func assertReadAnyValue(ctx *readerTestContext, r *Reader, value commontest.AnyValue) error { av := r.Any() if r.Error() != nil { return r.Error() } switch value.Kind { case commontest.NullValue: return commontest.AssertEqual(NullValue, av.Kind) case commontest.BoolValue: return commontest.AssertNoErrors(commontest.AssertEqual(BoolValue, av.Kind), commontest.AssertEqual(value.Bool, av.Bool)) case commontest.NumberValue: return commontest.AssertNoErrors(commontest.AssertEqual(NumberValue, av.Kind), commontest.AssertEqual(value.Number, av.Number)) case commontest.StringValue: return commontest.AssertNoErrors(commontest.AssertEqual(StringValue, av.Kind), commontest.AssertEqual(value.String, av.String)) case commontest.ArrayValue: if err := commontest.AssertEqual(ArrayValue, av.Kind); err != nil { return err } return assertReadArray(ctx, &av.Array, value) case commontest.ObjectValue: if err := commontest.AssertEqual(ObjectValue, av.Kind); err != nil { return err } return assertReadObject(ctx, &av.Object, value) } return nil } func (f readerErrorTestFactory) ExpectEOFError(err error) error { return tokenReaderErrorTestFactory{}.ExpectEOFError(err) } func (f readerErrorTestFactory) ExpectWrongTypeError(err error, expected commontest.ValueKind, variant commontest.ValueVariant, actual commontest.ValueKind) error { // Here our behavior is different from tokenReaderErrorTestFactory, because Reader has more possible // kinds of errors due to the convenience features for reading values as "some type *or* null". expectedError := TypeError{ Expected: valueKindFromTestValueKind(expected), Actual: valueKindFromTestValueKind(actual), } switch variant { case nullableValue, nullableNumberAsInt: expectedError.Nullable = true case nullableBoolIsNull: expectedError.Nullable = true expectedError.Expected = BoolValue case nullableIntIsNull, nullableFloatIsNull: expectedError.Nullable = true expectedError.Expected = NumberValue case nullableStringIsNull: expectedError.Nullable = true expectedError.Expected = StringValue case nullableArrayIsNull: expectedError.Nullable = true expectedError.Expected = ArrayValue case nullableObjectIsNull: expectedError.Nullable = true expectedError.Expected = ObjectValue } if expectedError.Nullable && valueKindFromTestValueKind(actual) == expectedError.Expected { return commontest.AssertEqual(shouldHaveBeenNullError, err) } if expectedError.Nullable && actual == commontest.NullValue { return commontest.AssertEqual(shouldNotHaveBeenNullError, err) } if te, ok := err.(TypeError); ok { expectedError.Offset = te.Offset if te == expectedError { return nil } } return fmt.Errorf("expected %T %+v, got %T %+v", expectedError, expectedError, err, err) } func (f readerErrorTestFactory) ExpectSyntaxError(err error) error { return tokenReaderErrorTestFactory{}.ExpectSyntaxError(err) } func TestReaderAddErrorDoesNotOverridePreviousError(t *testing.T) { fakeError := errors.New("sorry") r := NewReader([]byte(`"not a bool"`)) _ = r.Bool() r.AddError(fakeError) require.Error(t, r.Error()) require.NotEqual(t, fakeError, r.Error()) require.IsType(t, TypeError{}, r.Error()) } func TestReaderSetErrorOverridesPreviousError(t *testing.T) { fakeError := errors.New("sorry") r := NewReader([]byte(`"not a bool"`)) _ = r.Bool() r.ReplaceError(fakeError) require.Error(t, r.Error()) require.Equal(t, fakeError, r.Error()) } func TestReaderSkipValue(t *testing.T) { t.Run("Next() skips array element if it was not read", func(t *testing.T) { data := []byte(`["a", ["b1", "b2"], "c"]`) r := NewReader(data) arr := r.Array() require.NoError(t, r.Error()) require.True(t, arr.Next()) val1 := r.String() require.NoError(t, r.Error()) require.Equal(t, "a", val1) require.True(t, arr.Next()) require.True(t, arr.Next()) val3 := r.String() require.NoError(t, r.Error()) require.Equal(t, "c", val3) require.False(t, arr.Next()) }) t.Run("Next() skips property value if it was not read", func(t *testing.T) { data := []byte(`{"a":1, "b":{"b1":2, "b2":3}, "c":4}`) r := NewReader(data) obj := r.Object() require.NoError(t, r.Error()) require.True(t, obj.Next()) require.Equal(t, "a", string(obj.Name())) val1 := r.Int() require.NoError(t, r.Error()) require.Equal(t, 1, val1) require.True(t, obj.Next()) require.True(t, obj.Next()) require.Equal(t, "c", string(obj.Name())) val3 := r.Int() require.NoError(t, r.Error()) require.Equal(t, 4, val3) require.False(t, obj.Next()) }) } go-jsonstream-3.0.0/jreader/reader_unmarshal.go000066400000000000000000000014601430321663200215620ustar00rootroot00000000000000package jreader // UnmarshalJSONWithReader is a convenience method for implementing json.Marshaler to unmarshal from // a byte slice with the default TokenReader implementation. If an error occurs, it is converted to // the corresponding error type defined by the encoding/json package when applicable. // // This method will generally be less efficient than writing the exact same logic inline in a custom // UnmarshalJSON method for the object's specific type, because casting a pointer to an interface // (Readable) will force the object to be allocated on the heap if it was not already. func UnmarshalJSONWithReader(data []byte, readable Readable) error { r := NewReader(data) readable.ReadFromJSONReader(&r) if err := r.Error(); err != nil { return ToJSONError(err, readable) } return r.RequireEOF() } go-jsonstream-3.0.0/jreader/reader_unmarshal_test.go000066400000000000000000000020251430321663200226170ustar00rootroot00000000000000package jreader import ( "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" "github.com/stretchr/testify/require" ) func TestUnmarshalJSONWithReader(t *testing.T) { var val ExampleStructWrapper err := UnmarshalJSONWithReader(commontest.ExampleStructData, &val) require.NoError(t, err) require.Equal(t, ExampleStructWrapper(commontest.ExampleStructValue), val) } func TestUnmarshalJSONWithReaderReturnsErrorForNonWhitespaceDatasAfterEnd(t *testing.T) { var val ExampleStructWrapper badJSON := string(commontest.ExampleStructData) + "xxx" err := UnmarshalJSONWithReader([]byte(badJSON), &val) require.Error(t, err) require.Contains(t, err.Error(), "unexpected data after end") } func TestUnmarshalJSONWithReaderDisregardsWhitespaceAfterEnd(t *testing.T) { var val ExampleStructWrapper okJSON := string(commontest.ExampleStructData) + " \t\n\r " err := UnmarshalJSONWithReader([]byte(okJSON), &val) require.NoError(t, err) require.Equal(t, ExampleStructWrapper(commontest.ExampleStructValue), val) } go-jsonstream-3.0.0/jreader/testdata_test.go000066400000000000000000000025361430321663200211230ustar00rootroot00000000000000package jreader import ( "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" ) // ExampleStruct is defined in another package, so we need to wrap it in our own type to define methods on it. type ExampleStructWrapper commontest.ExampleStruct type ExampleStructWrapperWithRequiredProps commontest.ExampleStruct func (s *ExampleStructWrapper) ReadFromJSONReader(r *Reader) { for obj := r.Object(); obj.Next(); { switch string(obj.Name()) { case commontest.ExampleStructStringFieldName: s.StringField = r.String() case commontest.ExampleStructIntFieldName: s.IntField = r.Int() case commontest.ExampleStructOptBoolAsInterfaceFieldName: b, nonNull := r.BoolOrNull() if nonNull { s.OptBoolAsInterfaceField = b } else { s.OptBoolAsInterfaceField = nil } } } } func (s *ExampleStructWrapperWithRequiredProps) ReadFromJSONReader(r *Reader) { for obj := r.Object().WithRequiredProperties(commontest.ExampleStructRequiredFieldNames); obj.Next(); { switch string(obj.Name()) { case commontest.ExampleStructStringFieldName: s.StringField = r.String() case commontest.ExampleStructIntFieldName: s.IntField = r.Int() case commontest.ExampleStructOptBoolAsInterfaceFieldName: b, nonNull := r.BoolOrNull() if nonNull { s.OptBoolAsInterfaceField = b } else { s.OptBoolAsInterfaceField = nil } } } } go-jsonstream-3.0.0/jreader/token_reader_default.go000066400000000000000000000367151430321663200224270ustar00rootroot00000000000000//go:build !launchdarkly_easyjson // +build !launchdarkly_easyjson package jreader // This file defines the default implementation of the low-level JSON tokenizer. If the launchdarkly_easyjson // build tag is enabled, we use the easyjson adapter in token_reader_easyjson.go instead. These have the same // methods so the Reader code does not need to know which implementation we're using; however, we don't // actually define an interface for these, because calling the methods through an interface would limit // performance. import ( "bytes" "io" "strconv" "unicode" "unicode/utf8" ) var ( tokenNull = []byte("null") //nolint:gochecknoglobals tokenTrue = []byte("true") //nolint:gochecknoglobals tokenFalse = []byte("false") //nolint:gochecknoglobals ) type token struct { kind tokenKind boolValue bool numberValue float64 stringValue []byte delimiter byte } type tokenKind int const ( nullToken tokenKind = iota boolToken tokenKind = iota numberToken tokenKind = iota stringToken tokenKind = iota delimiterToken tokenKind = iota ) func (t token) valueKind() ValueKind { if t.kind == delimiterToken { if t.delimiter == '[' { return ArrayValue } if t.delimiter == '{' { return ObjectValue } } return valueKindFromTokenKind(t.kind) } func (t token) description() string { if t.kind == delimiterToken && t.delimiter != '[' && t.delimiter != '{' { return "'" + string(t.delimiter) + "'" } return t.valueKind().String() } type tokenReader struct { data []byte pos int len int hasUnread bool unreadToken token lastPos int } func newTokenReader(data []byte) tokenReader { tr := tokenReader{ data: data, pos: 0, len: len(data), } return tr } // EOF returns true if we are at the end of the input (not counting whitespace). func (r *tokenReader) EOF() bool { if r.hasUnread { return false } _, ok := r.skipWhitespaceAndReadByte() if !ok { return true } r.unreadByte() return false } // LastPos returns the byte offset within the input where we most recently started parsing a token. func (r *tokenReader) LastPos() int { return r.lastPos } func (r *tokenReader) getPos() int { if r.hasUnread { return r.lastPos } return r.pos } // Null returns (true, nil) if the next token is a null (consuming the token); (false, nil) if the next // token is not a null (not consuming the token); or (false, error) if the next token is not a valid // JSON value. // // This and all other tokenReader methods skip transparently past whitespace between tokens. func (r *tokenReader) Null() (bool, error) { t, err := r.next() if err != nil { return false, err } if t.kind == nullToken { return true, nil } r.putBack(t) if t.kind == delimiterToken && t.delimiter != '[' && t.delimiter != '{' { return false, SyntaxError{Message: errMsgUnexpectedChar, Value: string(t.delimiter), Offset: r.getPos()} } return false, nil } // Bool requires that the next token is a JSON boolean, returning its value if successful (consuming // the token), or an error if the next token is anything other than a JSON boolean. // // This and all other tokenReader methods skip transparently past whitespace between tokens. func (r *tokenReader) Bool() (bool, error) { t, err := r.consumeScalar(boolToken) return t.boolValue, err } // Bool requires that the next token is a JSON number, returning its value if successful (consuming // the token), or an error if the next token is anything other than a JSON number. // // This and all other tokenReader methods skip transparently past whitespace between tokens. func (r *tokenReader) Number() (float64, error) { t, err := r.consumeScalar(numberToken) return t.numberValue, err } // String requires that the next token is a JSON string, returning its value if successful (consuming // the token), or an error if the next token is anything other than a JSON string. // // This and all other tokenReader methods skip transparently past whitespace between tokens. func (r *tokenReader) String() (string, error) { t, err := r.consumeScalar(stringToken) return string(t.stringValue), err } // PropertyName requires that the next token is a JSON string and the token after that is a colon, // returning the string as a byte slice if successful, or an error otherwise. // // Returning the string as a byte slice avoids the overhead of allocating a string, since normally // the names of properties will not be retained as strings but are only compared to constants while // parsing an object. // // This and all other tokenReader methods skip transparently past whitespace between tokens. func (r *tokenReader) PropertyName() ([]byte, error) { t, err := r.consumeScalar(stringToken) if err != nil { return nil, err } b, ok := r.skipWhitespaceAndReadByte() if !ok { return nil, io.EOF } if b != ':' { r.unreadByte() return nil, r.syntaxErrorOnNextToken(errMsgExpectedColon) } return t.stringValue, nil } // Delimiter checks whether the next token is the specified ASCII delimiter character. If so, it // returns (true, nil) and consumes the token. If it is a delimiter, but not the same one, it // returns (false, nil) and does not consume the token. For anything else, it returns an error. // // This and all other tokenReader methods skip transparently past whitespace between tokens. func (r *tokenReader) Delimiter(delimiter byte) (bool, error) { if r.hasUnread { if r.unreadToken.kind == delimiterToken && r.unreadToken.delimiter == delimiter { r.hasUnread = false return true, nil } return false, nil } b, ok := r.skipWhitespaceAndReadByte() if !ok { return false, nil } if b == delimiter { return true, nil } r.unreadByte() // we'll back up and try to parse a token, to see if it's valid JSON or not token, err := r.next() if err != nil { return false, err // it was malformed JSON } r.putBack(token) // it was valid JSON, we just haven't hit that delimiter return false, nil } // EndDelimiterOrComma checks whether the next token is the specified ASCII delimiter character // or a comma. If it is the specified delimiter, it returns (true, nil) and consumes the token. // If it is a comma, it returns (false, nil) and consumes the token. For anything else, it // returns an error. The delimiter parameter will always be either '}' or ']'. func (r *tokenReader) EndDelimiterOrComma(delimiter byte) (bool, error) { if r.hasUnread { if r.unreadToken.kind == delimiterToken && (r.unreadToken.delimiter == delimiter || r.unreadToken.delimiter == ',') { r.hasUnread = false return r.unreadToken.delimiter == delimiter, nil } return false, SyntaxError{Message: badArrayOrObjectItemMessage(delimiter == '}'), Value: r.unreadToken.description(), Offset: r.lastPos} } b, ok := r.skipWhitespaceAndReadByte() if !ok { return false, io.EOF } if b == delimiter || b == ',' { return b == delimiter, nil } r.unreadByte() t, err := r.next() if err != nil { return false, err } return false, SyntaxError{Message: badArrayOrObjectItemMessage(delimiter == '}'), Value: t.description(), Offset: r.lastPos} } func badArrayOrObjectItemMessage(isObject bool) string { if isObject { return errMsgBadObjectItem } return errMsgBadArrayItem } // Any checks whether the next token is either a valid JSON scalar value or the opening delimiter of // an array or object value. If so, it returns (AnyValue, nil) and consumes the token; if not, it // returns an error. Unlike Reader.Any(), for array and object values it does not create an // ArrayState or ObjectState. func (r *tokenReader) Any() (AnyValue, error) { t, err := r.next() if err != nil { return AnyValue{}, err } switch t.kind { case boolToken: return AnyValue{Kind: BoolValue, Bool: t.boolValue}, nil case numberToken: return AnyValue{Kind: NumberValue, Number: t.numberValue}, nil case stringToken: return AnyValue{Kind: StringValue, String: string(t.stringValue)}, nil case delimiterToken: if t.delimiter == '[' { return AnyValue{Kind: ArrayValue}, nil } if t.delimiter == '{' { return AnyValue{Kind: ObjectValue}, nil } return AnyValue{}, SyntaxError{Message: errMsgUnexpectedChar, Value: string(t.delimiter), Offset: r.lastPos} default: return AnyValue{Kind: NullValue}, nil } } // Attempts to parse and consume the next token, ignoring whitespace. A token is either a valid JSON scalar // value or an ASCII delimiter character. If a token was previously unread using putBack, it consumes that // instead. func (r *tokenReader) next() (token, error) { if r.hasUnread { r.hasUnread = false return r.unreadToken, nil } b, ok := r.skipWhitespaceAndReadByte() if !ok { return token{}, io.EOF } switch { // We can get away with reading bytes instead of runes because the JSON spec doesn't allow multi-byte // characters except within a string literal. case b >= 'a' && b <= 'z': n := r.consumeASCIILowercaseAlphabeticChars() + 1 id := r.data[r.lastPos : r.lastPos+n] if b == 'f' && bytes.Equal(id, tokenFalse) { return token{kind: boolToken, boolValue: false}, nil } if b == 't' && bytes.Equal(id, tokenTrue) { return token{kind: boolToken, boolValue: true}, nil } if b == 'n' && bytes.Equal(id, tokenNull) { return token{kind: nullToken}, nil } return token{}, SyntaxError{Message: errMsgUnexpectedSymbol, Value: string(id), Offset: r.lastPos} case (b >= '0' && b <= '9') || b == '-': if n, ok := r.readNumber(b); ok { return token{kind: numberToken, numberValue: n}, nil } return token{}, SyntaxError{Message: errMsgInvalidNumber, Offset: r.lastPos} case b == '"': s, err := r.readString() if err != nil { return token{}, err } return token{kind: stringToken, stringValue: s}, nil case b == '[', b == ']', b == '{', b == '}', b == ':', b == ',': return token{kind: delimiterToken, delimiter: b}, nil } return token{}, SyntaxError{Message: errMsgUnexpectedChar, Value: string(b), Offset: r.lastPos} } func (r *tokenReader) putBack(token token) { r.unreadToken = token r.hasUnread = true } func (r *tokenReader) consumeScalar(kind tokenKind) (token, error) { t, err := r.next() if err != nil { return token{}, err } if t.kind == kind { return t, nil } if t.kind == delimiterToken && t.delimiter != '[' && t.delimiter != '{' { return token{}, SyntaxError{Message: errMsgUnexpectedChar, Value: string(t.delimiter), Offset: r.LastPos()} } return token{}, TypeError{Expected: valueKindFromTokenKind(kind), Actual: t.valueKind(), Offset: r.LastPos()} } func (r *tokenReader) readByte() (byte, bool) { if r.pos >= r.len { return 0, false } b := r.data[r.pos] r.pos++ return b, true } func (r *tokenReader) unreadByte() { r.pos-- } func (r *tokenReader) skipWhitespaceAndReadByte() (byte, bool) { for { ch, ok := r.readByte() if !ok { return 0, false } if !unicode.IsSpace(rune(ch)) { r.lastPos = r.pos - 1 return ch, true } } } func (r *tokenReader) consumeASCIILowercaseAlphabeticChars() int { n := 0 for { ch, ok := r.readByte() if !ok { break } if ch < 'a' || ch > 'z' { r.unreadByte() break } n++ } return n } func (r *tokenReader) readNumber(first byte) (float64, bool) { //nolint:unparam startPos := r.lastPos isFloat := false var ch byte var ok bool for { ch, ok = r.readByte() if !ok { break } if (ch < '0' || ch > '9') && !(ch == '.' && !isFloat) { break } if ch == '.' { isFloat = true } } hasExponent := false if ch == 'e' || ch == 'E' { // exponent must match this regex: [eE][-+]?[0-9]+ ch, ok = r.readByte() if !ok { return 0, false } if ch == '+' || ch == '-' { //nolint:gocritic } else if ch >= '0' && ch <= '9' { r.unreadByte() } else { return 0, false } for { ch, ok = r.readByte() if !ok { break } if ch < '0' || ch > '9' { r.unreadByte() break } hasExponent = true } if !hasExponent { return 0, false } isFloat = true } else { //nolint:gocritic if ok { r.unreadByte() } } chars := r.data[startPos:r.pos] if isFloat { // Unfortunately, strconv.ParseFloat requires a string - there is no []byte equivalent. This means we can't // avoid a heap allocation here. Easyjson works around this by creating an unsafe string that points directly // at the existing bytes, but in our default implementation we can't use unsafe. n, err := strconv.ParseFloat(string(chars), 64) return n, err == nil } else { //nolint:revive n, ok := parseIntFromBytes(chars) return float64(n), ok } } func (r *tokenReader) readString() ([]byte, error) { startPos := r.pos // the opening quote mark has already been read var chars []byte haveEscaped := false var reader bytes.Reader // bytes.Reader understands multi-byte characters reader.Reset(r.data) _, _ = reader.Seek(int64(r.pos), io.SeekStart) for { ch, _, err := reader.ReadRune() if err != nil { return nil, r.syntaxErrorOnLastToken(errMsgInvalidString) } if ch == '"' { break } if ch != '\\' { if haveEscaped { chars = appendRune(chars, ch) } continue } if !haveEscaped { pos := (r.len - reader.Len()) - 1 // don't include the backslash we just read chars = make([]byte, pos-startPos, pos-startPos+20) if pos > startPos { copy(chars, r.data[startPos:pos]) } haveEscaped = true } ch, _, err = reader.ReadRune() if err != nil { return nil, r.syntaxErrorOnLastToken(errMsgInvalidString) } switch ch { case '"', '\\', '/': chars = appendRune(chars, ch) case 'b': chars = appendRune(chars, '\b') case 'f': chars = appendRune(chars, '\f') case 'n': chars = appendRune(chars, '\n') case 'r': chars = appendRune(chars, '\r') case 't': chars = appendRune(chars, '\t') case 'u': if ch, ok := readHexChar(&reader); ok { chars = appendRune(chars, ch) } else { return nil, r.syntaxErrorOnLastToken(errMsgInvalidString) } default: return nil, r.syntaxErrorOnLastToken(errMsgInvalidString) } } r.pos = r.len - reader.Len() if haveEscaped { if len(chars) == 0 { return nil, nil } return chars, nil } else { //nolint:revive pos := r.pos - 1 if pos <= startPos { return nil, nil } return r.data[startPos:pos], nil } } func readHexChar(reader *bytes.Reader) (rune, bool) { var digits [4]byte for i := 0; i < 4; i++ { ch, err := reader.ReadByte() if err != nil || !((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) { return 0, false } digits[i] = ch } n, _ := strconv.ParseUint(string(digits[:]), 16, 32) return rune(n), true } func (r *tokenReader) syntaxErrorOnLastToken(msg string) error { //nolint:unparam return SyntaxError{Message: msg, Offset: r.LastPos()} } func (r *tokenReader) syntaxErrorOnNextToken(msg string) error { t, err := r.next() if err != nil { return err } return SyntaxError{Message: msg, Value: t.description(), Offset: r.LastPos()} } // This is faster than creating a string to pass to strconv.Atoi. func parseIntFromBytes(chars []byte) (int64, bool) { negate := false p := 0 var ret int64 if len(chars) == 0 { return 0, false } if chars[0] == '-' { negate = true p++ if p == len(chars) { return 0, false } } for p < len(chars) { ret = ret*10 + int64(chars[p]-'0') p++ } if negate { ret = -ret } return ret, true } func appendRune(out []byte, ch rune) []byte { var encodedRune [10]byte n := utf8.EncodeRune(encodedRune[0:10], ch) return append(out, encodedRune[0:n]...) } func valueKindFromTokenKind(k tokenKind) ValueKind { switch k { case nullToken: return NullValue case boolToken: return BoolValue case numberToken: return NumberValue case stringToken: return StringValue } return -1 } go-jsonstream-3.0.0/jreader/token_reader_easyjson.go000066400000000000000000000156111430321663200226260ustar00rootroot00000000000000//go:build launchdarkly_easyjson // +build launchdarkly_easyjson package jreader // This file defines the easyjson-based implementation of the low-level JSON tokenizer, which is used instead // of token_reader_default.go if the launchdarkly_easyjson build tag is enabled. // // For the contract governing the behavior of the exported methods in this type, see the comments on the // corresponding methods in token_reader_default.go. import ( "fmt" "strings" "github.com/mailru/easyjson/jlexer" ) type tokenReader struct { // We might be initialized either with a pointer to an existing Lexer, in which case we'll use that. pLexer *jlexer.Lexer // Or, we might be initialized with a byte slice so we must create our own Lexer. We'd like to avoid // allocating that on the heap, so we'll store it here. inlineLexer jlexer.Lexer posBeforeCallingNull int posAfterCallingNull int posBeforeValue int } func newTokenReader(data []byte) tokenReader { return tokenReader{inlineLexer: jlexer.Lexer{Data: data}} } func newTokenReaderFromEasyjsonLexer(lexer *jlexer.Lexer) tokenReader { return tokenReader{pLexer: lexer} } func (tr *tokenReader) EOF() bool { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } if pLexer.Error() != nil { return true } pLexer.Consumed() return pLexer.Error() == nil } func (tr *tokenReader) LastPos() int { if tr.pLexer == nil { return tr.inlineLexer.GetPos() } return tr.pLexer.GetPos() } func (tr *tokenReader) Null() (bool, error) { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } // The "posBefore/posAfter" stuff is because it's not possible to rewind a Lexer. If we call Null(), // and the value isn't null, the Lexer will cache that token for the next read, but GetPos() will // still be pointing *after* the token and that'll screw up our error reporting. tr.posBeforeCallingNull = pLexer.GetPos() if pLexer.IsNull() { // Lexer.IsNull can return a misleading true value if there's a parsing error if err := pLexer.Error(); err != nil { return false, tr.translateLexerError() } pLexer.Null() return true, nil } tr.posAfterCallingNull = pLexer.GetPos() return false, tr.translateLexerError() } func (tr *tokenReader) Bool() (bool, error) { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } tr.markPosBeforeValue() val := pLexer.Bool() if pLexer.Error() == nil { return val, nil } return false, tr.translateLexerErrorWithExpectedType(BoolValue) } func (tr *tokenReader) Number() (float64, error) { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } tr.markPosBeforeValue() val := pLexer.Float64() if pLexer.Error() == nil { return val, nil } return 0, tr.translateLexerErrorWithExpectedType(NumberValue) } func (tr *tokenReader) String() (string, error) { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } tr.markPosBeforeValue() val := pLexer.String() if pLexer.Error() == nil { return val, nil } return "", tr.translateLexerErrorWithExpectedType(StringValue) } func (tr *tokenReader) PropertyName() ([]byte, error) { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } val := pLexer.UnsafeBytes() if err := pLexer.Error(); err != nil { return nil, tr.translateLexerError() } pLexer.WantColon() pLexer.FetchToken() return val, tr.translateLexerError() } func (tr *tokenReader) Delimiter(delim byte) (bool, error) { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } if err := pLexer.Error(); err != nil { return false, tr.translateLexerError() } found := false if pLexer.IsDelim(byte(delim)) { pLexer.Delim(delim) found = true } // IsDelim can return a misleading true value if there's a parsing error if err := pLexer.Error(); err != nil { return false, tr.translateLexerError() } return found, nil } func (tr *tokenReader) EndDelimiterOrComma(delim byte) (bool, error) { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } if pLexer.Error() != nil { return false, tr.translateLexerError() } pLexer.WantComma() if pLexer.IsDelim(delim) { pLexer.Delim(delim) return true, nil } return false, tr.translateLexerError() } func (tr *tokenReader) Any() (AnyValue, error) { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } value, err := readAnyValue(pLexer) if err != nil { return AnyValue{}, tr.translateLexerError() } return value, nil } func (tr *tokenReader) lexerError() error { if tr.pLexer == nil { return tr.inlineLexer.Error() } return tr.pLexer.Error() } func readAnyValue(lexer *jlexer.Lexer) (AnyValue, error) { if lexer.IsDelim('[') { // IsDelim can return a misleading true value if there's a parsing error if err := lexer.Error(); err != nil { return AnyValue{}, err } lexer.Delim('[') return AnyValue{Kind: ArrayValue}, nil } if lexer.IsDelim('{') { if err := lexer.Error(); err != nil { return AnyValue{}, err } lexer.Delim('{') return AnyValue{Kind: ObjectValue}, nil } intf := lexer.Interface() if err := lexer.Error(); err != nil { return AnyValue{}, err } if intf == nil { return AnyValue{Kind: NullValue}, nil } switch v := intf.(type) { case bool: return AnyValue{Kind: BoolValue, Bool: v}, nil case int: return AnyValue{Kind: NumberValue, Number: float64(v)}, nil case float64: return AnyValue{Kind: NumberValue, Number: v}, nil case string: return AnyValue{Kind: StringValue, String: v}, nil } return AnyValue{}, fmt.Errorf("Lexer.Interface() returned unrecognized type %T", intf) } func (tr *tokenReader) markPosBeforeValue() { pos := tr.LastPos() if pos == tr.posAfterCallingNull { pos = tr.posBeforeCallingNull } } func (tr *tokenReader) translateLexerError() error { return tr.translateLexerErrorWithExpectedType(-1) } func (tr *tokenReader) translateLexerErrorWithExpectedType(expectedType ValueKind) error { pLexer := tr.pLexer if pLexer == nil { pLexer = &tr.inlineLexer } originalError := pLexer.Error() if originalError == nil { return nil } le, ok := originalError.(*jlexer.LexerError) if !ok { return originalError } if strings.HasPrefix(le.Reason, "expected ") && expectedType >= 0 { // LexerError is not very useful for determining what the invalid token was, because it tends to // leave the Data property empty and put the Offset property *after* the bad token. Fortunately, // it's very easy to create a new Lexer to re-parse from where we started. tempLexer := jlexer.Lexer{Data: pLexer.Data[tr.posBeforeValue:]} value, err := readAnyValue(&tempLexer) if err != nil { return translateLexerParseError(err) } return TypeError{Expected: expectedType, Actual: value.Kind, Offset: tr.posBeforeValue} } return translateLexerParseError(originalError) } func translateLexerParseError(err error) error { if le, ok := err.(*jlexer.LexerError); ok { return SyntaxError{Message: strings.TrimPrefix(le.Reason, "parse error: "), Offset: le.Offset} } return err } go-jsonstream-3.0.0/jreader/token_reader_test.go000066400000000000000000000124251430321663200217520ustar00rootroot00000000000000package jreader import ( "errors" "fmt" "io" "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" ) // This uses the framework defined in ReaderTestSuite to exercise any TokenReader implementation // with a large number of valid and invalid JSON inputs. type tokenReaderTestContext struct { input []byte tr *tokenReader } type tokenReaderValueTestFactory struct{} type tokenReaderErrorTestFactory struct{} type TokenReaderTestSuite struct { Factory func([]byte) *tokenReader } func TestTokenReader(t *testing.T) { s := TokenReaderTestSuite{ Factory: func(input []byte) *tokenReader { tr := newTokenReader(input) return &tr }, } s.Run(t) } func (s TokenReaderTestSuite) Run(t *testing.T) { rs := commontest.ReaderTestSuite{ ContextFactory: func(input []byte) commontest.TestContext { return &tokenReaderTestContext{ input: input, tr: s.Factory(input), } }, ValueTestFactory: tokenReaderValueTestFactory{}, ReadErrorTestFactory: tokenReaderErrorTestFactory{}, } rs.Run(t) } func (c tokenReaderTestContext) JSONData() []byte { return c.input } func (f tokenReaderValueTestFactory) EOF() commontest.Action { return func(c commontest.TestContext) error { return commontest.AssertTrue(c.(*tokenReaderTestContext).tr.EOF(), "unexpected data after end") } } func (f tokenReaderValueTestFactory) Null() commontest.Action { return func(c commontest.TestContext) error { tr := c.(*tokenReaderTestContext).tr ok, err := tr.Null() if err != nil { return err } if !ok { any, _ := tr.Any() return TypeError{Expected: NullValue, Actual: any.Kind} } return nil } } func (f tokenReaderValueTestFactory) Variants(value commontest.AnyValue) []commontest.ValueVariant { // TokenReader does not need to use ValueVariants because it does not have any ambiguity // about what type a value is being read as. return nil } func (f tokenReaderValueTestFactory) Value(value commontest.AnyValue, variant commontest.ValueVariant) commontest.Action { return func(c commontest.TestContext) error { ctx := c.(*tokenReaderTestContext) tr := ctx.tr switch value.Kind { case commontest.NullValue: return f.Null()(c) case commontest.BoolValue: gotVal, err := tr.Bool() return commontest.AssertNoErrors(err, commontest.AssertEqual(value.Bool, gotVal)) case commontest.NumberValue: gotVal, err := tr.Number() return commontest.AssertNoErrors(err, commontest.AssertEqual(value.Number, gotVal)) case commontest.StringValue: gotVal, err := tr.String() return commontest.AssertNoErrors(err, commontest.AssertEqual(value.String, gotVal)) case commontest.ArrayValue: gotDelim, err := tr.Delimiter('[') if err != nil { return err } if !gotDelim { return errors.New("expected start of array") } first := true for _, e := range value.Array { if !first { isEnd, err := tr.EndDelimiterOrComma(']') if err := commontest.AssertNoErrors(err, commontest.AssertTrue(!isEnd, "array ended too soon")); err != nil { return err } } first = false if err := e(c); err != nil { return err } } isEnd, err := tr.EndDelimiterOrComma(']') return commontest.AssertNoErrors(err, commontest.AssertTrue(isEnd, "expected end of array")) case commontest.ObjectValue: gotDelim, err := tr.Delimiter('{') if err != nil { return err } if !gotDelim { return errors.New("expected start of object") } first := true for _, p := range value.Object { if !first { isEnd, err := tr.EndDelimiterOrComma('}') if err := commontest.AssertNoErrors(err, commontest.AssertTrue(!isEnd, "object ended too soon")); err != nil { return err } } first = false name, err := tr.PropertyName() if err := commontest.AssertNoErrors(err, commontest.AssertEqual(string(name), p.Name)); err != nil { return err } if err := p.Action(c); err != nil { return err } } isEnd, err := tr.EndDelimiterOrComma('}') return commontest.AssertNoErrors(err, commontest.AssertTrue(isEnd, "expected end of object")) } return nil } } func (f tokenReaderErrorTestFactory) ExpectEOFError(err error) error { return commontest.AssertEqual(io.EOF, err) } func (f tokenReaderErrorTestFactory) ExpectWrongTypeError(err error, expected commontest.ValueKind, variant commontest.ValueVariant, actual commontest.ValueKind) error { if te, ok := err.(TypeError); ok { if te.Actual == valueKindFromTestValueKind(actual) && te.Expected == valueKindFromTestValueKind(expected) { return nil } } return fmt.Errorf("expected TypeError{Expected: %s, Actual: %s}, got %T %+v", valueKindFromTestValueKind(expected), valueKindFromTestValueKind(actual), err, err) } func (f tokenReaderErrorTestFactory) ExpectSyntaxError(err error) error { if _, ok := err.(SyntaxError); ok { return nil } return fmt.Errorf("expected SyntaxError, got %T %+v", err, err) } func valueKindFromTestValueKind(kind commontest.ValueKind) ValueKind { switch kind { case commontest.NullValue: return NullValue case commontest.BoolValue: return BoolValue case commontest.NumberValue: return NumberValue case commontest.StringValue: return StringValue case commontest.ArrayValue: return ArrayValue case commontest.ObjectValue: return ObjectValue } return NullValue } go-jsonstream-3.0.0/jwriter/000077500000000000000000000000001430321663200157705ustar00rootroot00000000000000go-jsonstream-3.0.0/jwriter/interfaces.go000066400000000000000000000006341430321663200204450ustar00rootroot00000000000000package jwriter // Writable is an interface for types that can write their data to a Writer. type Writable interface { // WriteToJSONWriter writes JSON content to the Writer. // // This method does not need to return an error value. If the Writer encounters an error during output // generation, it will remember its own error state, which can be detected with Writer.Error(). WriteToJSONWriter(*Writer) } go-jsonstream-3.0.0/jwriter/json_marshal_comparative_benchmark_test.go000066400000000000000000000060161430321663200264450ustar00rootroot00000000000000package jwriter import ( "bytes" "encoding/json" "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" ) // These benchmarks perform equivalent actions to the ones in writer_benchmark_test.go, but using // the default reflection-based mechanism from the json/encoding package, so we can see how much // less efficient that is than our default implementation and the easyjson implementation. func BenchmarkJSONMarshalComparatives(b *testing.B) { b.Run("Null", benchmarkWriteNullJSONMarshal) b.Run("Boolean", benchmarkWriteBooleanJSONMarshal) b.Run("NumberInt", benchmarkWriteNumberIntJSONMarshal) b.Run("NumberFloat", benchmarkWriteNumberFloatJSONMarshal) b.Run("String", benchmarkWriteStringJSONMarshal) b.Run("ArrayOfBools", benchmarkWriteArrayOfBoolsJSONMarshal) b.Run("ArrayOfStrings", benchmarkWriteArrayOfStringsJSONMarshal) b.Run("Object", benchmarkWriteObjectJSONMarshal) } func benchmarkWriteNullJSONMarshal(b *testing.B) { expected := []byte("null") b.ResetTimer() for i := 0; i < b.N; i++ { data, err := json.Marshal(nil) benchmarkExpectJSONMarshalOutput(b, err, data, expected) } } func benchmarkWriteBooleanJSONMarshal(b *testing.B) { expected := []byte("true") b.ResetTimer() for i := 0; i < b.N; i++ { data, err := json.Marshal(true) benchmarkExpectJSONMarshalOutput(b, err, data, expected) } } func benchmarkWriteNumberIntJSONMarshal(b *testing.B) { expected := []byte("123") b.ResetTimer() for i := 0; i < b.N; i++ { data, err := json.Marshal(123) benchmarkExpectJSONMarshalOutput(b, err, data, expected) } } func benchmarkWriteNumberFloatJSONMarshal(b *testing.B) { expected := []byte("1234.5") b.ResetTimer() for i := 0; i < b.N; i++ { data, err := json.Marshal(1234.5) benchmarkExpectJSONMarshalOutput(b, err, data, expected) } } func benchmarkWriteStringJSONMarshal(b *testing.B) { expected := []byte(`"abc"`) val := "abc" b.ResetTimer() for i := 0; i < b.N; i++ { data, err := json.Marshal(val) benchmarkExpectJSONMarshalOutput(b, err, data, expected) } } func benchmarkWriteArrayOfBoolsJSONMarshal(b *testing.B) { vals := commontest.MakeBools() expected := commontest.MakeBoolsJSON(vals) b.ResetTimer() for i := 0; i < b.N; i++ { data, err := json.Marshal(vals) benchmarkExpectJSONMarshalOutput(b, err, data, expected) } } func benchmarkWriteArrayOfStringsJSONMarshal(b *testing.B) { vals := commontest.MakeStrings() expected := commontest.MakeStringsJSON(vals) b.ResetTimer() for i := 0; i < b.N; i++ { data, err := json.Marshal(vals) benchmarkExpectJSONMarshalOutput(b, err, data, expected) } } func benchmarkWriteObjectJSONMarshal(b *testing.B) { for i := 0; i < b.N; i++ { data, err := json.Marshal(ExampleStructWrapper(commontest.ExampleStructValue)) benchmarkExpectJSONMarshalOutput(b, err, data, commontest.ExampleStructData) } } func benchmarkExpectJSONMarshalOutput(b *testing.B, err error, actualJSON []byte, expectedJSON []byte) { if err != nil { b.Error(err) b.FailNow() } if !bytes.Equal(expectedJSON, actualJSON) { b.FailNow() } } go-jsonstream-3.0.0/jwriter/no_op_writer.go000066400000000000000000000004511430321663200210250ustar00rootroot00000000000000package jwriter var noOpWriter = makeNoOpWriter() //nolint:gochecknoglobals func makeNoOpWriter() Writer { w := Writer{} w.AddError(noOpWriterError{}) return w } type noOpWriterError struct{} func (noOpWriterError) Error() string { return "this is a stub Writer that produces no output" } go-jsonstream-3.0.0/jwriter/package_example_test.go000066400000000000000000000004721430321663200224670ustar00rootroot00000000000000package jwriter import "fmt" func Example() { w := NewWriter() obj := w.Object() obj.Name("propertyName").String("propertyValue") obj.End() if err := w.Error(); err != nil { fmt.Println("error:", err.Error()) } else { fmt.Println(string(w.Bytes())) } // Output: {"propertyName":"propertyValue"} } go-jsonstream-3.0.0/jwriter/package_info.go000066400000000000000000000041711430321663200207300ustar00rootroot00000000000000// Package jwriter provides an efficient mechanism for writing JSON data sequentially. // // The high-level API for this package, Writer, is designed to facilitate writing custom JSON // marshaling logic concisely and reliably. Output is buffered in memory. // // import ( // "gopkg.in/launchdarkly/jsonstream.v1/jwriter" // ) // // type myStruct struct { // value int // } // // func (s myStruct) WriteToJSONWriter(w *jwriter.Writer) { // obj := w.Object() // writing a JSON object structure like {"value":2} // obj.Property("value").Int(s.value) // obj.End() // } // // func PrintMyStructJSON(s myStruct) { // w := jwriter.NewWriter() // s.WriteToJSONWriter(&w) // fmt.Println(string(w.Bytes()) // } // // Output can optionally be dumped to an io.Writer at intervals to avoid allocating a large buffer: // // func WriteToHTTPResponse(s myStruct, resp http.ResponseWriter) { // resp.Header.Add("Content-Type", "application/json") // w := jwriter.NewStreamingWriter(resp, 1000) // myStruct.WriteToJSONWriter(&w) // } // // The underlying low-level token writing mechanism has two available implementations. The default // implementation has no external dependencies. For interoperability with the easyjson library // (https://github.com/mailru/easyjson), there is also an implementation that delegates to the // easyjson streaming writer; this is enabled by setting the build tag "launchdarkly_easyjson". // Be aware that by default, easyjson uses Go's "unsafe" package (https://pkg.go.dev/unsafe), // which may not be available on all platforms. // // Setting the "launchdarkly_easyjson" tag also adds a new constructor function, // NewWriterFromEasyJSONWriter, allowing Writer-based code to send output directly to an // existing EasyJSON jwriter.Writer. This may be desirable in order to define common marshaling // logic that may be used with or without EasyJSON. For example: // // import ( // ej_jwriter "github.com/mailru/easyjson/jwriter" // ) // // func (s myStruct) MarshalEasyJSON(w *ej_jwriter.Writer) { // ww := jwriter.NewWriterFromEasyJSONWriter(w) // s.WriteToJSONWriter(&ww) // } package jwriter go-jsonstream-3.0.0/jwriter/streamable_buffer.go000066400000000000000000000023061430321663200217700ustar00rootroot00000000000000package jwriter import ( "bytes" "io" ) type streamableBuffer struct { buf bytes.Buffer dest io.Writer destErr error chunkSize int } func (b *streamableBuffer) Bytes() []byte { return b.buf.Bytes() } func (b *streamableBuffer) Grow(n int) { b.buf.Grow(n) } func (b *streamableBuffer) SetStreamingWriter(w io.Writer, chunkSize int) { b.dest = w b.chunkSize = chunkSize } func (b *streamableBuffer) Flush() error { if b.dest != nil { if b.buf.Len() > 0 { if b.destErr == nil { data := b.buf.Bytes() _, b.destErr = b.dest.Write(data) } b.buf.Reset() return b.destErr } } return nil } func (b *streamableBuffer) maybeFlush() { if b.dest != nil && b.buf.Len() >= b.chunkSize { _ = b.Flush() } } func (b *streamableBuffer) GetWriterError() error { return b.destErr } func (b *streamableBuffer) Write(data []byte) { _, _ = b.buf.Write(data) b.maybeFlush() } func (b *streamableBuffer) WriteByte(data byte) { //nolint:govet _ = b.buf.WriteByte(data) b.maybeFlush() } func (b *streamableBuffer) WriteRune(ch rune) { _, _ = b.buf.WriteRune(ch) b.maybeFlush() } func (b *streamableBuffer) WriteString(s string) { _, _ = b.buf.WriteString(s) b.maybeFlush() } go-jsonstream-3.0.0/jwriter/streamable_buffer_test.go000066400000000000000000000033271430321663200230330ustar00rootroot00000000000000package jwriter import ( "bytes" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestStreamableBufferInMemoryMode(t *testing.T) { var b streamableBuffer expected := writeTestDataToBuffer(&b) assert.Equal(t, expected, string(b.Bytes())) } func TestStreamableBufferFlushDoesNothingByDefault(t *testing.T) { var b streamableBuffer expected := writeTestDataToBuffer(&b) require.NoError(t, b.Flush()) assert.Equal(t, expected, string(b.Bytes())) } func TestStreamableBufferStreamingMode(t *testing.T) { t.Run("verify full data", func(t *testing.T) { var b streamableBuffer var target bytes.Buffer b.SetStreamingWriter(&target, 20) expected := writeTestDataToBuffer(&b) b.Flush() assert.Equal(t, expected, target.String()) }) t.Run("data is flushed incrementally", func(t *testing.T) { var b streamableBuffer var target bytes.Buffer b.SetStreamingWriter(&target, 10) b.WriteString("12345678") assert.Len(t, target.Bytes(), 0) b.WriteString("90") assert.Equal(t, "1234567890", target.String()) b.WriteString("abcdefghijklm") assert.Equal(t, "1234567890abcdefghijklm", target.String()) b.WriteString("nopqrstu") assert.Equal(t, "1234567890abcdefghijklm", target.String()) b.WriteRune('v') b.WriteByte('w') assert.Equal(t, "1234567890abcdefghijklmnopqrstuvw", target.String()) b.WriteString("xyz") b.Flush() assert.Equal(t, "1234567890abcdefghijklmnopqrstuvwxyz", target.String()) }) } func writeTestDataToBuffer(b *streamableBuffer) string { s := "abcdefghijklmnopqrstuvwxyz🐈" expected := "" for i := 0; i < 100; i++ { b.WriteString(s) expected += s b.WriteRune('$') b.WriteByte(' ') expected += "$ " } return expected } go-jsonstream-3.0.0/jwriter/test_behavior_default.go000066400000000000000000000006241430321663200226630ustar00rootroot00000000000000//go:build !launchdarkly_easyjson // +build !launchdarkly_easyjson package jwriter // This function tells the writer tests that we shouldn't expect to see hex escape sequences in the output. // Our default implementation doesn't use them, whereas easyjson does; either way is valid in JSON. func tokenWriterWillEncodeAsHex(ch rune) bool { //nolint:deadcode,unused // linter is confused return false } go-jsonstream-3.0.0/jwriter/test_behavior_easyjson.go000066400000000000000000000006171430321663200230740ustar00rootroot00000000000000//go:build launchdarkly_easyjson // +build launchdarkly_easyjson package jwriter // This function tells the writer tests that we should expect to see hex escape sequences in the output // for certain characters, because that's the behavior of easyjson. func tokenWriterWillEncodeAsHex(ch rune) bool { //nolint:deadcode,unused // linter is confused return ch != '\t' && ch != '\n' && ch != '\r' } go-jsonstream-3.0.0/jwriter/testdata_test.go000066400000000000000000000012171430321663200211700ustar00rootroot00000000000000package jwriter import ( "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" ) // ExampleStruct is defined in another package, so we need to wrap it in our own type to define methods on it. type ExampleStructWrapper commontest.ExampleStruct func (s ExampleStructWrapper) WriteToJSONWriter(w *Writer) { obj := w.Object() obj.Name(commontest.ExampleStructStringFieldName).String(s.StringField) obj.Name(commontest.ExampleStructIntFieldName).Int(s.IntField) obj.Name(commontest.ExampleStructOptBoolAsInterfaceFieldName) if s.OptBoolAsInterfaceField == nil { w.Null() } else { w.Bool(s.OptBoolAsInterfaceField.(bool)) } obj.End() } go-jsonstream-3.0.0/jwriter/token_writer_default.go000066400000000000000000000110421430321663200225350ustar00rootroot00000000000000//go:build !launchdarkly_easyjson // +build !launchdarkly_easyjson package jwriter import ( "encoding/json" "io" "strconv" "unicode/utf8" ) // This file defines the default implementation of the low-level JSON token writer. If the launchdarkly_easyjson // build tag is enabled, we use the easyjson adapter in token_writer_easyjson.go instead. These have the same // methods so the Writer code does not need to know which implementation we're using; however, we don't // actually define an interface for these, because calling the methods through an interface would limit // performance. var ( tokenNull = []byte("null") //nolint:gochecknoglobals tokenTrue = []byte("true") //nolint:gochecknoglobals tokenFalse = []byte("false") //nolint:gochecknoglobals ) type tokenWriter struct { buf streamableBuffer tempBytes [50]byte } func newTokenWriter() tokenWriter { return tokenWriter{} } func newStreamingTokenWriter(dest io.Writer, bufferSize int) tokenWriter { tw := tokenWriter{} tw.buf.Grow(bufferSize) tw.buf.SetStreamingWriter(dest, bufferSize) return tw } // Bytes returns the full encoded byte slice. // // If the buffer is in a failed state from a previous invalid operation, Bytes() returns any data written // so far. func (tw *tokenWriter) Bytes() []byte { return tw.buf.Bytes() } // Grow expands the internal buffer by the specified number of bytes. It is the same as calling Grow // on a bytes.Buffer. func (tw *tokenWriter) Grow(n int) { tw.buf.Grow(n) } // Flush writes any remaining in-memory output to the underlying Writer, if this is a streaming buffer // created with newStreamingTokenWriter. It has no effect otherwise. func (tw *tokenWriter) Flush() error { return tw.buf.Flush() } // Null writes a JSON null. func (tw *tokenWriter) Null() error { tw.buf.Write(tokenNull) return tw.buf.GetWriterError() } // Bool writes a JSON boolean. func (tw *tokenWriter) Bool(value bool) error { var out []byte if value { out = tokenTrue } else { out = tokenFalse } tw.buf.Write(out) return tw.buf.GetWriterError() } // Int writes an integer JSON number. func (tw *tokenWriter) Int(value int) error { if value == 0 { tw.buf.WriteByte('0') } else { out := tw.tempBytes[0:0] out = strconv.AppendInt(out, int64(value), 10) tw.buf.Write(out) } return tw.buf.GetWriterError() } // Float64 writes a JSON number. func (tw *tokenWriter) Float64(value float64) error { if value == 0 { tw.buf.WriteByte('0') } else { i := int(value) if float64(i) == value { return tw.Int(i) } out := tw.tempBytes[0:0] out = strconv.AppendFloat(out, value, 'g', -1, 64) tw.buf.Write(out) } return tw.buf.GetWriterError() } // String writes a JSON string. func (tw *tokenWriter) String(value string) error { return tw.writeQuotedString(value) } // Raw writes a preformatted chunk of JSON data. func (tw *tokenWriter) Raw(value json.RawMessage) error { tw.buf.Write(value) return tw.buf.GetWriterError() } // PropertyName writes a JSON object property name followed by a colon. func (tw *tokenWriter) PropertyName(name string) error { if err := tw.String(name); err != nil { return err } tw.buf.WriteByte(':') return tw.buf.GetWriterError() } // Delimiter writes a single character which must be a valid JSON delimiter ('{', ',', etc.). func (tw *tokenWriter) Delimiter(delimiter byte) error { tw.buf.WriteByte(delimiter) return tw.buf.GetWriterError() } func (tw *tokenWriter) writeQuotedString(s string) error { // This is basically the same logic used internally by json.Marshal tw.buf.WriteByte('"') start := 0 for i := 0; i < len(s); { aByte := s[i] if aByte < ' ' || aByte == '"' || aByte == '\\' { if i > start { tw.buf.WriteString(s[start:i]) } tw.writeEscapedChar(aByte) i++ start = i } else { if aByte < utf8.RuneSelf { // single-byte character i++ } else { _, size := utf8.DecodeRuneInString(s[i:]) i += size } } } if start < len(s) { tw.buf.WriteString(s[start:]) } tw.buf.WriteByte('"') return tw.buf.GetWriterError() } func (tw *tokenWriter) writeEscapedChar(ch byte) { out := tw.tempBytes[0:2] out[0] = '\\' switch ch { case '\b': out[1] = 'b' case '\t': out[1] = 't' case '\n': out[1] = 'n' case '\f': out[1] = 'f' case '\r': out[1] = 'r' case '"': out[1] = '"' case '\\': out[1] = '\\' default: out[1] = 'u' out = append(out, '0') out = append(out, '0') hexChars := make([]byte, 0, 4) hexChars = strconv.AppendInt(hexChars, int64(ch), 16) if len(hexChars) < 2 { out = append(out, '0') } out = append(out, hexChars...) } tw.buf.Write(out) } go-jsonstream-3.0.0/jwriter/token_writer_easyjson.go000066400000000000000000000070231430321663200227500ustar00rootroot00000000000000//go:build launchdarkly_easyjson // +build launchdarkly_easyjson package jwriter // This file defines the easyjson-based implementation of the low-level JSON writer, which is used instead // of token_writer_default.go if the launchdarkly_easyjson build tag is enabled. // // For the contract governing the behavior of the exported methods in this type, see the comments on the // corresponding methods in token_writer_default.go. import ( "encoding/json" "io" ejwriter "github.com/mailru/easyjson/jwriter" ) var nullToken = []byte("null") type tokenWriter struct { // We might be initialized either with a pointer to an existing Writer, in which case we'll use that. pWriter *ejwriter.Writer // Or, we might be initialized from scratch so we must create our own Writer. We'd like to avoid // allocating that on the heap, so we'll store it here. inlineWriter ejwriter.Writer targetIOWriter io.Writer targetBufferSize int } func newTokenWriter() tokenWriter { return tokenWriter{} } func newTokenWriterFromEasyjsonWriter(writer *ejwriter.Writer) tokenWriter { return tokenWriter{pWriter: writer} } func newStreamingTokenWriter(dest io.Writer, bufferSize int) tokenWriter { tw := tokenWriter{targetIOWriter: dest, targetBufferSize: bufferSize} tw.inlineWriter.Buffer.EnsureSpace(bufferSize) return tw } func (tw *tokenWriter) Bytes() []byte { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } bytes, _ := pWriter.BuildBytes(nil) return bytes } func (tw *tokenWriter) Grow(n int) { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } pWriter.Buffer.EnsureSpace(n) } func (tw *tokenWriter) Flush() error { if tw.targetIOWriter == nil { return nil } _, err := tw.inlineWriter.DumpTo(tw.targetIOWriter) return err } func (tw *tokenWriter) Null() error { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } pWriter.Raw(nullToken, nil) return tw.maybeFlush() } func (tw *tokenWriter) Bool(value bool) error { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } pWriter.Bool(value) return tw.maybeFlush() } func (tw *tokenWriter) Int(value int) error { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } pWriter.Int(value) return tw.maybeFlush() } func (tw *tokenWriter) Float64(value float64) error { i := int(value) if float64(i) == value { return tw.Int(i) } pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } pWriter.Float64(value) return tw.maybeFlush() } func (tw *tokenWriter) String(value string) error { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } pWriter.String(value) return tw.maybeFlush() } func (tw *tokenWriter) Raw(value json.RawMessage) error { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } pWriter.Raw(value, nil) return tw.maybeFlush() } func (tw *tokenWriter) PropertyName(name string) error { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } pWriter.String(name) pWriter.RawByte(':') return tw.maybeFlush() } func (tw *tokenWriter) Delimiter(delimiter byte) error { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } pWriter.RawByte(delimiter) return tw.maybeFlush() } func (tw *tokenWriter) maybeFlush() error { pWriter := tw.pWriter if pWriter == nil { pWriter = &tw.inlineWriter } if pWriter.Error != nil { return pWriter.Error } if tw.targetIOWriter == nil || pWriter.Buffer.Size() < tw.targetBufferSize { return nil } return tw.Flush() } go-jsonstream-3.0.0/jwriter/token_writer_test.go000066400000000000000000000076471430321663200221100ustar00rootroot00000000000000package jwriter import ( "encoding/json" "fmt" "strconv" "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" ) // This uses the framework defined in WriterTestSuite to exercise any TokenWriter implementation // with a large number of JSON output permutations. type tokenWriterTestContext struct { tw *tokenWriter getOutput func() []byte } type tokenWriterValueTestFactory struct{} const ( writeNumberAsInt commontest.ValueVariant = "int:" ) var ( variantsForScalarValues = []commontest.ValueVariant{"", commontest.UntypedVariant} variantsForWritingNumbers = []commontest.ValueVariant{"", writeNumberAsInt, commontest.UntypedVariant} ) type tokenWriterTestSuite struct { Factory func() (*tokenWriter, func() []byte) EncodeAsHex func(rune) bool } func TestTokenWriter(t *testing.T) { s := tokenWriterTestSuite{ Factory: func() (*tokenWriter, func() []byte) { tw := newTokenWriter() return &tw, tw.Bytes }, } s.Run(t) } func (s tokenWriterTestSuite) Run(t *testing.T) { ws := commontest.WriterTestSuite{ ContextFactory: func() commontest.TestContext { tw, getOutput := s.Factory() return &tokenWriterTestContext{ tw: tw, getOutput: getOutput, } }, ValueTestFactory: tokenWriterValueTestFactory{}, EncodeAsHex: tokenWriterWillEncodeAsHex, } ws.Run(t) } func (c tokenWriterTestContext) JSONData() []byte { return c.getOutput() } func (f tokenWriterValueTestFactory) EOF() commontest.Action { return func(c commontest.TestContext) error { return nil } } func (f tokenWriterValueTestFactory) Variants(value commontest.AnyValue) []commontest.ValueVariant { // Integer values can be written using either Int() or Float64(). if value.Kind == commontest.NumberValue && float64(int(value.Number)) == value.Number { return variantsForWritingNumbers } if value.Kind != commontest.ArrayValue && value.Kind != commontest.ObjectValue { return variantsForScalarValues } return nil } func (f tokenWriterValueTestFactory) Value(value commontest.AnyValue, variant commontest.ValueVariant) commontest.Action { return func(c commontest.TestContext) error { ctx := c.(*tokenWriterTestContext) tw := ctx.tw switch value.Kind { case commontest.NullValue: if variant == commontest.UntypedVariant { return tw.Raw(json.RawMessage(`null`)) } return tw.Null() case commontest.BoolValue: if variant == commontest.UntypedVariant { return tw.Raw(json.RawMessage(fmt.Sprintf("%t", value.Bool))) } return tw.Bool(value.Bool) case commontest.NumberValue: if variant == commontest.UntypedVariant { return tw.Raw(json.RawMessage(strconv.FormatFloat(value.Number, 'f', -1, 64))) } if variant == writeNumberAsInt { return tw.Int(int(value.Number)) } else { return tw.Float64(value.Number) } case commontest.StringValue: if variant == commontest.UntypedVariant { // Use our own encoder to encode the string, but then write it with Raw() tw1 := newTokenWriter() _ = tw1.String(value.String) return tw.Raw(json.RawMessage(tw1.Bytes())) } return tw.String(value.String) case commontest.ArrayValue: if err := tw.Delimiter('['); err != nil { return err } first := true for _, e := range value.Array { if !first { if err := tw.Delimiter(','); err != nil { return err } } first = false if err := e(c); err != nil { return err } } return tw.Delimiter(']') case commontest.ObjectValue: if err := tw.Delimiter('{'); err != nil { return err } first := true for _, p := range value.Object { if !first { if err := tw.Delimiter(','); err != nil { return err } } first = false if err := tw.String(p.Name); err != nil { return err } if err := tw.Delimiter(':'); err != nil { return err } if err := p.Action(c); err != nil { return err } } return tw.Delimiter('}') } return nil } } go-jsonstream-3.0.0/jwriter/writer.go000066400000000000000000000122011430321663200176270ustar00rootroot00000000000000package jwriter import ( "encoding/json" ) // Writer is a high-level API for writing JSON data sequentially. // // It is designed to make writing custom marshallers for application types as convenient as // possible. The general usage pattern is as follows: // // - There is one method for each JSON data type. // // - For writing array or object structures, the Array and Object methods return a struct that // keeps track of additional writer state while that structure is being written. // // - If any method encounters an error (for instance, if an underlying io.Writer returns an error // when using NewStreamingWriter), or if an error is explicitly raised with AddError, the Writer // permanently enters a failed state and remembers that error; all subsequent method calls for // producing output will be ignored. type Writer struct { tw tokenWriter err error state writerState } // writerState keeps track of semantic state such as whether we're within an array. This has // stack-like behavior, but to avoid allocating an actual stack, we use ArrayState and // ObjectState to hold previous values of this struct. type writerState struct { inArray bool arrayHasItems bool } // Bytes returns the full contents of the output buffer. func (w *Writer) Bytes() []byte { return w.tw.Bytes() } // Error returns the first error, if any, that occurred during output generation. If there have // been no errors, it returns nil. // // As soon as any operation fails at any level, either in the JSON encoding or in writing to an // underlying io.Writer, the Writer remembers the error and will generate no further output. func (w *Writer) Error() error { return w.err } // AddError sets the error state if an error has not already been recorded. func (w *Writer) AddError(err error) { if w.err == nil { w.err = err } } // Flush writes any remaining in-memory output to the underlying io.Writer, if this is a streaming // writer created with NewStreamingWriter. It has no effect otherwise. func (w *Writer) Flush() error { return w.tw.Flush() } // Null writes a JSON null value to the output. func (w *Writer) Null() { if w.beforeValue() { w.AddError(w.tw.Null()) } } // Bool writes a JSON boolean value to the output. func (w *Writer) Bool(value bool) { if w.beforeValue() { w.AddError(w.tw.Bool(value)) } } // BoolOrNull is a shortcut for calling Bool(value) if isDefined is true, or else // Null(). func (w *Writer) BoolOrNull(isDefined bool, value bool) { if isDefined { w.Bool(value) } else { w.Null() } } // Int writes a JSON numeric value to the output. func (w *Writer) Int(value int) { if w.beforeValue() { w.AddError(w.tw.Int(value)) } } // IntOrNull is a shortcut for calling Int(value) if isDefined is true, or else // Null(). func (w *Writer) IntOrNull(isDefined bool, value int) { if isDefined { w.Int(value) } else { w.Null() } } // Float64 writes a JSON numeric value to the output. func (w *Writer) Float64(value float64) { if w.beforeValue() { w.AddError(w.tw.Float64(value)) } } // Float64OrNull is a shortcut for calling Float64(value) if isDefined is true, or else // Null(). func (w *Writer) Float64OrNull(isDefined bool, value float64) { if isDefined { w.Float64(value) } else { w.Null() } } // String writes a JSON string value to the output, adding quotes and performing any necessary escaping. func (w *Writer) String(value string) { if w.beforeValue() { w.AddError(w.tw.String(value)) } } // StringOrNull is a shortcut for calling String(value) if isDefined is true, or else // Null(). func (w *Writer) StringOrNull(isDefined bool, value string) { if isDefined { w.String(value) } else { w.Null() } } // Raw writes a pre-encoded JSON value to the output as-is. Its format is assumed to be correct; this // operation will not fail unless it is not permitted to write a value at this point. func (w *Writer) Raw(value json.RawMessage) { if value == nil { w.Null() } else if w.beforeValue() { w.AddError(w.tw.Raw(value)) } } // Array begins writing a JSON array to the output. It returns an ArrayState that provides the array // formatting; you must call ArrayState.End() when finished. func (w *Writer) Array() ArrayState { if w.beforeValue() { if err := w.tw.Delimiter('['); err != nil { w.err = err return ArrayState{} } previousState := w.state w.state = writerState{inArray: true} return ArrayState{w: w, previousState: previousState} } return ArrayState{} } // Object begins writing a JSON object to the output. It returns an ObjectState that provides the // object formatting; you must call ObjectState.End() when finished. func (w *Writer) Object() ObjectState { if w.beforeValue() { if err := w.tw.Delimiter('{'); err != nil { w.err = err return ObjectState{} } previousState := w.state w.state = writerState{inArray: false} return ObjectState{w: w, previousState: previousState} } return ObjectState{} } func (w *Writer) beforeValue() bool { if w.err != nil { return false } if w.state.inArray { if w.state.arrayHasItems { if err := w.tw.Delimiter(','); err != nil { w.AddError(err) return false } } else { w.state.arrayHasItems = true } } return true } go-jsonstream-3.0.0/jwriter/writer_array.go000066400000000000000000000035131430321663200210330ustar00rootroot00000000000000package jwriter import "encoding/json" // ArrayState is a decorator that manages the state of a JSON array that is in the process of being // written. // // Calling Writer.Array() or ObjectState.Array() creates an ArrayState. Until ArrayState.End() is // called, writing any value to either the ArrayState or the Writer will cause commas to be added // between values as needed. type ArrayState struct { w *Writer previousState writerState } // Null is equivalent to writer.Null(). func (arr *ArrayState) Null() { if arr.w != nil { arr.w.Null() } } // Bool is equivalent to writer.Bool(value). func (arr *ArrayState) Bool(value bool) { if arr.w != nil { arr.w.Bool(value) } } // Int is equivalent to writer.Int(value). func (arr *ArrayState) Int(value int) { if arr.w != nil { arr.w.Int(value) } } // Float64 is equivalent to writer.Float64(value). func (arr *ArrayState) Float64(value float64) { if arr.w != nil { arr.w.Float64(value) } } // String is equivalent to writer.String(value). func (arr *ArrayState) String(value string) { if arr.w != nil { arr.w.String(value) } } // Array is equivalent to calling writer.Array(), to create a nested array. func (arr *ArrayState) Array() ArrayState { if arr.w != nil { return arr.w.Array() } return ArrayState{} } // Object is equivalent to calling writer.Object(), to create a nested object. func (arr *ArrayState) Object() ObjectState { if arr.w != nil { return arr.w.Object() } return ObjectState{} } // Raw is equivalent to calling writer.Raw(). func (arr *ArrayState) Raw(value json.RawMessage) { if arr.w != nil { arr.w.Raw(value) } } // End writes the closing delimiter of the array. func (arr *ArrayState) End() { if arr.w == nil || arr.w.err != nil { return } arr.w.AddError(arr.w.tw.Delimiter(']')) arr.w.state = arr.previousState arr.w = nil } go-jsonstream-3.0.0/jwriter/writer_array_examples_test.go000066400000000000000000000031551430321663200237720ustar00rootroot00000000000000package jwriter import ( "encoding/json" "fmt" ) func ExampleArrayState_Null() { w := NewWriter() arr := w.Array() arr.Null() arr.Null() arr.End() fmt.Println(string(w.Bytes())) // Output: [null,null] } func ExampleArrayState_Bool() { w := NewWriter() arr := w.Array() arr.Bool(true) arr.Bool(false) arr.End() fmt.Println(string(w.Bytes())) // Output: [true,false] } func ExampleArrayState_Int() { w := NewWriter() arr := w.Array() arr.Int(123) arr.Int(456) arr.End() fmt.Println(string(w.Bytes())) // Output: [123,456] } func ExampleArrayState_Float64() { w := NewWriter() arr := w.Array() arr.Float64(1234.5) arr.Float64(6) arr.End() fmt.Println(string(w.Bytes())) // Output: [1234.5,6] } func ExampleArrayState_String() { w := NewWriter() arr := w.Array() arr.String(`string says "hello"`) arr.String("ok") arr.End() fmt.Println(string(w.Bytes())) // Output: ["string says \"hello\"","ok"] } func ExampleArrayState_Array() { w := NewWriter() arr := w.Array() arr.Int(1) subArr := arr.Array() subArr.Int(2) subArr.Int(3) subArr.End() arr.Int(4) arr.End() fmt.Println(string(w.Bytes())) // Output: [1,[2,3],4] } func ExampleArrayState_Object() { w := NewWriter() arr := w.Array() obj1 := arr.Object() obj1.Name("value").Int(1) obj1.End() obj2 := arr.Object() obj2.Name("value").Int(2) obj2.End() arr.End() fmt.Println(string(w.Bytes())) // Output: [{"value":1},{"value":2}] } func ExampleArrayState_Raw() { data := json.RawMessage(`{"value":1}`) w := NewWriter() arr := w.Array() arr.Raw(data) arr.End() fmt.Println(string(w.Bytes())) // Output: [{"value":1}] } go-jsonstream-3.0.0/jwriter/writer_array_test.go000066400000000000000000000007611430321663200220740ustar00rootroot00000000000000package jwriter import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestArrayState(t *testing.T) { w := NewWriter() a := w.Array() a.Null() a.Bool(true) a.Int(3) a.Float64(4.5) a.String("five") aa := a.Array() aa.Int(6) aa.End() ao := a.Object() ao.Name("seven").Int(7) ao.End() a.End() require.NoError(t, w.Error()) expected := `[null,true,3,4.5,"five",[6],{"seven":7}]` assert.JSONEq(t, expected, string(w.Bytes())) } go-jsonstream-3.0.0/jwriter/writer_benchmark_test.go000066400000000000000000000062461430321663200227140ustar00rootroot00000000000000package jwriter import ( "bytes" "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" ) func BenchmarkWriteNull(b *testing.B) { expected := []byte("null") b.ResetTimer() for i := 0; i < b.N; i++ { w := NewWriter() w.Null() benchmarkExpectWriterOutput(b, &w, expected) } } func BenchmarkWriteBoolean(b *testing.B) { expected := []byte("true") b.ResetTimer() for i := 0; i < b.N; i++ { w := NewWriter() w.Bool(true) benchmarkExpectWriterOutput(b, &w, expected) } } func BenchmarkWriteNumberInt(b *testing.B) { expected := []byte("123") b.ResetTimer() for i := 0; i < b.N; i++ { w := NewWriter() w.Int(123) benchmarkExpectWriterOutput(b, &w, expected) } } func BenchmarkWriteNumberFloat(b *testing.B) { expected := []byte("1234.5") b.ResetTimer() for i := 0; i < b.N; i++ { w := NewWriter() w.Float64(1234.5) benchmarkExpectWriterOutput(b, &w, expected) } } func BenchmarkWriteString(b *testing.B) { expected := []byte(`"abc"`) b.ResetTimer() for i := 0; i < b.N; i++ { w := NewWriter() w.String("abc") benchmarkExpectWriterOutput(b, &w, expected) } } func BenchmarkWriteArrayOfBools(b *testing.B) { vals := commontest.MakeBools() expected := commontest.MakeBoolsJSON(vals) b.ResetTimer() for i := 0; i < b.N; i++ { w := NewWriter() arr := w.Array() for _, val := range vals { arr.Bool(val) } arr.End() benchmarkExpectWriterOutput(b, &w, expected) } } func BenchmarkWriteArrayOfStrings(b *testing.B) { vals := commontest.MakeStrings() expected := commontest.MakeStringsJSON(vals) b.ResetTimer() for i := 0; i < b.N; i++ { w := NewWriter() arr := w.Array() for _, val := range vals { arr.String(val) } arr.End() benchmarkExpectWriterOutput(b, &w, expected) } } func BenchmarkWriteObject(b *testing.B) { for i := 0; i < b.N; i++ { w := NewWriter() ExampleStructWrapper(commontest.ExampleStructValue).WriteToJSONWriter(&w) benchmarkExpectWriterOutput(b, &w, commontest.ExampleStructData) } } func benchmarkExpectWriterOutput(b *testing.B, w *Writer, expectedJSON []byte) { if err := w.Error(); err != nil { b.Error(err) b.FailNow() } if !bytes.Equal(expectedJSON, w.Bytes()) { b.FailNow() } } func BenchmarkWriteObjectToNoOpWriterNoAllocs(b *testing.B) { // The purpose of this benchmark is to ensure that nothing is escaping to the heap simply // as result of calling the Name or Maybe methods (as it might if we hadn't been // careful about our use of pointers). We're preinitializing the Writer to already have an // error, so it won't produce any output. w := NewWriter() obj := w.Object() w.AddError(noOpWriterError{}) b.ResetTimer() for i := 0; i < b.N; i++ { obj.Name("prop1").Int(1) obj.Maybe("prop2", true).Int(2) obj.Maybe("prop3", false).Int(3) } } func BenchmarkStreamingWriterArrayOfStrings(b *testing.B) { vals := commontest.MakeStrings() expected := commontest.MakeStringsJSON(vals) b.ResetTimer() for i := 0; i < b.N; i++ { var buf bytes.Buffer w := NewStreamingWriter(&buf, 50) arr := w.Array() for _, val := range vals { arr.String(val) } arr.End() w.Flush() output := buf.Bytes() if !bytes.Equal(expected, output) { b.FailNow() } } } go-jsonstream-3.0.0/jwriter/writer_examples_test.go000066400000000000000000000050001430321663200225630ustar00rootroot00000000000000package jwriter import ( "encoding/json" "errors" "fmt" "os" ) func ExampleNewWriter() { w := NewWriter() obj := w.Object() obj.Name("property").String("value") obj.End() fmt.Println(string(w.Bytes())) // Output: {"property":"value"} } func ExampleNewStreamingWriter() { w := NewStreamingWriter(os.Stdout, 10) obj := w.Object() obj.Name("property").String("value") obj.End() w.Flush() // Output: {"property":"value"} } func ExampleWriter_AddError() { w := NewWriter() obj := w.Object() obj.Name("prop1").Bool(true) w.AddError(errors.New("sorry, we can't serialize this after all")) obj.Name("prop2").Bool(true) // no output is generated here because the Writer has already failed fmt.Println("error is:", w.Error()) fmt.Println("buffer is:", string(w.Bytes())) // Output: error is: sorry, we can't serialize this after all // buffer is: {"prop1":true } func ExampleWriter_Null() { w := NewWriter() w.Null() fmt.Println(string(w.Bytes())) // Output: null } func ExampleWriter_Bool() { w := NewWriter() w.Bool(true) fmt.Println(string(w.Bytes())) // Output: true } func ExampleWriter_BoolOrNull() { w := NewWriter() w.BoolOrNull(false, true) fmt.Println(string(w.Bytes())) // Output: null } func ExampleWriter_Int() { w := NewWriter() w.Int(123) fmt.Println(string(w.Bytes())) // Output: 123 } func ExampleWriter_IntOrNull() { w := NewWriter() w.IntOrNull(false, 1) fmt.Println(string(w.Bytes())) // Output: null } func ExampleWriter_Float64() { w := NewWriter() w.Float64(1234.5) fmt.Println(string(w.Bytes())) // Output: 1234.5 } func ExampleWriter_Float64OrNull() { w := NewWriter() w.Float64OrNull(false, 1) fmt.Println(string(w.Bytes())) // Output: null } func ExampleWriter_String() { w := NewWriter() w.String(`string says "hello"`) fmt.Println(string(w.Bytes())) // Output: "string says \"hello\"" } func ExampleWriter_StringOrNull() { w := NewWriter() w.StringOrNull(false, "no") fmt.Println(string(w.Bytes())) // Output: null } func ExampleWriter_Array() { w := NewWriter() arr := w.Array() arr.Bool(true) arr.Int(3) arr.End() fmt.Println(string(w.Bytes())) // Output: [true,3] } func ExampleWriter_Object() { w := NewWriter() obj := w.Object() obj.Name("boolProperty").Bool(true) obj.Name("intProperty").Int(3) obj.End() fmt.Println(string(w.Bytes())) // Output: {"boolProperty":true,"intProperty":3} } func ExampleWriter_Raw() { data := json.RawMessage(`{"value":1}`) w := NewWriter() w.Raw(data) fmt.Println(string(w.Bytes())) // Output: {"value":1} } go-jsonstream-3.0.0/jwriter/writer_init_default.go000066400000000000000000000022661430321663200223700ustar00rootroot00000000000000package jwriter import "io" // NewWriter creates a Writer that will buffer its entire output in memory. // // This function returns the struct by value (Writer, not *Writer). This avoids the overhead of a // heap allocation since, in typical usage, the Writer will not escape the scope in which it was // declared and can remain on the stack. func NewWriter() Writer { return Writer{tw: newTokenWriter()} } // NewStreamingWriter creates a Writer that will buffer a limited amount of its output in memory // and dump the output to the specified io.Writer whenever the buffer is full. You should also // call Flush at the end of your output to ensure that any remaining buffered output is flushed. // // If the Writer returns an error at any point, it enters a failed state and will not try to // write any more data to the target. // // This function returns the struct by value (Writer, not *Writer). This avoids the overhead of a // heap allocation since, in typical usage, the Writer will not escape the scope in which it was // declared and can remain on the stack. func NewStreamingWriter(target io.Writer, bufferSize int) Writer { return Writer{tw: newStreamingTokenWriter(target, bufferSize)} } go-jsonstream-3.0.0/jwriter/writer_init_easyjson.go000066400000000000000000000015441430321663200225750ustar00rootroot00000000000000//go:build launchdarkly_easyjson // +build launchdarkly_easyjson package jwriter import ( ejwriter "github.com/mailru/easyjson/jwriter" ) // NewWriterFromEasyJSONWriter creates a Writer that produces JSON output through the specified easyjson // jwriter.Writer. // // This function is only available in code that was compiled with the build tag "launchdarkly_easyjson". // Its purpose is to allow custom marshaling code that is based on the Reader API to be used as // efficiently as possible within other data structures that are being marshaled with easyjson. // Directly using the same Writer that is already being used is more efficient than producing an // intermediate byte slice and then passing that data to the Writer. func NewWriterFromEasyJSONWriter(writer *ejwriter.Writer) Writer { return Writer{ tw: newTokenWriterFromEasyjsonWriter(writer), } } go-jsonstream-3.0.0/jwriter/writer_init_easyjson_test.go000066400000000000000000000022151430321663200236300ustar00rootroot00000000000000//go:build launchdarkly_easyjson // +build launchdarkly_easyjson package jwriter import ( "testing" ejwriter "github.com/mailru/easyjson/jwriter" "github.com/stretchr/testify/require" ) func TestNewWriterFromEasyJSONWriter(t *testing.T) { expectedOutput := `[1,{"property":2},3]` ejw := ejwriter.Writer{} // Write the first part of a JSON array directly with the easyjson Writer ejw.RawByte('[') require.NoError(t, ejw.Error) ejw.Int(1) require.NoError(t, ejw.Error) ejw.RawByte(',') require.NoError(t, ejw.Error) // Now pick up where we left off and use our Writer to write {"property":2} writer := NewWriterFromEasyJSONWriter(&ejw) obj := writer.Object() require.NoError(t, writer.Error()) obj.Name("property").Int(2) require.NoError(t, writer.Error()) obj.End() // The easyjson Writer should be left in the proper state to write the rest of the stream require.NoError(t, ejw.Error) ejw.RawByte(',') require.NoError(t, ejw.Error) ejw.Int(3) require.NoError(t, ejw.Error) ejw.RawByte(']') require.NoError(t, ejw.Error) output, err := ejw.BuildBytes() require.NoError(t, err) require.Equal(t, expectedOutput, string(output)) } go-jsonstream-3.0.0/jwriter/writer_marshal.go000066400000000000000000000006011430321663200213370ustar00rootroot00000000000000package jwriter // MarshalJSONWithWriter is a convenience method for implementing json.Marshaler to marshal to a // byte slice with the default TokenWriter implementation. func MarshalJSONWithWriter(writable Writable) ([]byte, error) { w := NewWriter() w.tw.Grow(1000) writable.WriteToJSONWriter(&w) if err := w.Error(); err != nil { return nil, err } return w.Bytes(), nil } go-jsonstream-3.0.0/jwriter/writer_marshal_test.go000066400000000000000000000005471430321663200224070ustar00rootroot00000000000000package jwriter import ( "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" "github.com/stretchr/testify/assert" ) func TestMarshalJSONWithWriter(t *testing.T) { data, err := MarshalJSONWithWriter(ExampleStructWrapper(commontest.ExampleStructValue)) assert.NoError(t, err) assert.Equal(t, commontest.ExampleStructData, data) } go-jsonstream-3.0.0/jwriter/writer_object.go000066400000000000000000000033341430321663200211640ustar00rootroot00000000000000package jwriter // ObjectState is a decorator that writes values to an underlying Writer within the context of a // JSON object, adding property names and commas between values as appropriate. type ObjectState struct { w *Writer hasItems bool previousState writerState } // Name writes an object property name and a colon. You can then use Writer methods to write // the property value. The return value is the same as the underlying Writer, so you can chain // method calls: // // obj.Name("myBooleanProperty").Bool(true) func (obj *ObjectState) Name(name string) *Writer { if obj.w == nil || obj.w.err != nil { return &noOpWriter } if obj.hasItems { if err := obj.w.tw.Delimiter(','); err != nil { obj.w.AddError(err) return obj.w } } obj.hasItems = true obj.w.AddError(obj.w.tw.PropertyName(name)) return obj.w } // Maybe writes an object property name conditionally depending on a boolean parameter. // If shouldWrite is true, this behaves the same as Property(name). However, if shouldWrite is false, // it does not write a property name and instead of returning the underlying Writer, it returns // a stub Writer that does not produce any output. This allows you to chain method calls without // having to use an if statement. // // obj.Maybe(shouldWeIncludeTheProperty, "myBooleanProperty").Bool(true) func (obj *ObjectState) Maybe(name string, shouldWrite bool) *Writer { if obj.w == nil { return &noOpWriter } if shouldWrite { return obj.Name(name) } return &noOpWriter } // End writes the closing delimiter of the object. func (obj *ObjectState) End() { if obj.w == nil || obj.w.err != nil { return } obj.w.AddError(obj.w.tw.Delimiter('}')) obj.w.state = obj.previousState obj.w = nil } go-jsonstream-3.0.0/jwriter/writer_object_examples_test.go000066400000000000000000000010771430321663200241230ustar00rootroot00000000000000package jwriter import ( "fmt" ) func ExampleObjectState_Name() { myCustomMarshaler := func(w *Writer) { subObject := w.Object() subObject.Name("yes").Bool(true) subObject.End() } w := NewWriter() obj := w.Object() myCustomMarshaler(obj.Name("subObject")) obj.End() fmt.Println(string(w.Bytes())) // Output: {"subObject":{"yes":true}} } func ExampleObjectState_Maybe() { w := NewWriter() obj := w.Object() obj.Maybe("notPresent", false).Int(1) obj.Maybe("present", true).Int(2) obj.End() fmt.Println(string(w.Bytes())) // Output: {"present":2} } go-jsonstream-3.0.0/jwriter/writer_object_test.go000066400000000000000000000011541430321663200222210ustar00rootroot00000000000000package jwriter import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestObjectState(t *testing.T) { w := NewWriter() o := w.Object() o.Name("prop1").Bool(true) o.Maybe("prop2", true).Bool(true) o.Maybe("shouldNotWriteThis", false).Bool(true) oa := o.Name("nestedArray").Array() oa.Int(1) oa.End() oo := o.Name("nestedObject").Object() oo.Name("eleven").Int(11) oo.End() o.End() require.NoError(t, w.Error()) expected := `{"prop1":true,"prop2":true,"nestedArray":[1],"nestedObject":{"eleven":11}}` assert.JSONEq(t, expected, string(w.Bytes())) } go-jsonstream-3.0.0/jwriter/writer_streaming_default_test.go000066400000000000000000000015301430321663200244460ustar00rootroot00000000000000//go:build !launchdarkly_easyjson // +build !launchdarkly_easyjson package jwriter import ( "bytes" "testing" "github.com/stretchr/testify/require" ) func TestStreamingWriterWritesToTargetInChunks(t *testing.T) { buf := bytes.NewBuffer(nil) w := NewStreamingWriter(buf, 10) expected := "" arr := w.Array() require.Equal(t, expected, buf.String()) arr.Bool(true) require.Equal(t, expected, buf.String()) arr.String("abc") expected += `[true,"abc` require.Equal(t, expected, buf.String()) arr.Int(33) require.Equal(t, expected, buf.String()) arr.Null() require.Equal(t, expected, buf.String()) arr.Float64(2.5) expected += `",33,null,` require.Equal(t, expected, buf.String()) arr.End() require.Equal(t, expected, buf.String()) require.NoError(t, w.Flush()) expected += `2.5]` require.Equal(t, expected, buf.String()) } go-jsonstream-3.0.0/jwriter/writer_streaming_easyjson_test.go000066400000000000000000000021701430321663200246560ustar00rootroot00000000000000//go:build launchdarkly_easyjson // +build launchdarkly_easyjson package jwriter // The expectations in this test are slightly different from the non-easyjson version in // writer_streaming_default_test.go, because in the easyjson implementation we don't have quite as // much control over when the buffer gets flushed, so it will only be flushed at the end of a write. import ( "bytes" "testing" "github.com/stretchr/testify/require" ) func TestStreamingWriterWritesToTargetInChunks(t *testing.T) { buf := bytes.NewBuffer(nil) w := NewStreamingWriter(buf, 10) expected := "" arr := w.Array() require.Equal(t, expected, buf.String()) arr.Bool(true) require.Equal(t, expected, buf.String()) arr.String("abc") expected += `[true,"abc"` require.Equal(t, expected, buf.String()) arr.Int(33) require.Equal(t, expected, buf.String()) arr.Null() require.Equal(t, expected, buf.String()) arr.Float64(2.5) expected += `,33,null,2.5` require.Equal(t, expected, buf.String()) arr.End() require.Equal(t, expected, buf.String()) require.NoError(t, w.Flush()) expected += `]` require.Equal(t, expected, buf.String()) } go-jsonstream-3.0.0/jwriter/writer_test.go000066400000000000000000000055011430321663200206730ustar00rootroot00000000000000package jwriter import ( "encoding/json" "fmt" "strconv" "testing" "github.com/launchdarkly/go-jsonstream/v3/internal/commontest" ) // This uses the framework defined in the commontest package to exercise Writer with a large // number of JSON output permutations, in conjunction with DefaultTokenWriter. type writerTestContext struct { w *Writer } type writerValueTestFactory struct{} func TestWriter(t *testing.T) { s := commontest.WriterTestSuite{ ContextFactory: func() commontest.TestContext { w := NewWriter() return &writerTestContext{ w: &w, } }, ValueTestFactory: writerValueTestFactory{}, EncodeAsHex: tokenWriterWillEncodeAsHex, } s.Run(t) } func (c writerTestContext) JSONData() []byte { return c.w.tw.Bytes() } func (f writerValueTestFactory) EOF() commontest.Action { return func(c commontest.TestContext) error { return c.(*writerTestContext).w.Error() } } func (f writerValueTestFactory) Variants(value commontest.AnyValue) []commontest.ValueVariant { // Integer values can be written using either Int() or Float64(). if value.Kind == commontest.NumberValue && float64(int(value.Number)) == value.Number { return variantsForWritingNumbers } if value.Kind != commontest.ArrayValue && value.Kind != commontest.ObjectValue { return variantsForScalarValues } return nil } func (f writerValueTestFactory) Value(value commontest.AnyValue, variant commontest.ValueVariant) commontest.Action { return func(c commontest.TestContext) error { ctx := c.(*writerTestContext) w := ctx.w switch value.Kind { case commontest.NullValue: if variant == commontest.UntypedVariant { w.Raw(json.RawMessage(`null`)) return w.Error() } w.Null() case commontest.BoolValue: if variant == commontest.UntypedVariant { w.Raw(json.RawMessage(fmt.Sprintf("%t", value.Bool))) return w.Error() } w.Bool(value.Bool) case commontest.NumberValue: if variant == commontest.UntypedVariant { w.Raw(json.RawMessage(strconv.FormatFloat(value.Number, 'f', -1, 64))) return w.Error() } if variant == writeNumberAsInt { w.Int(int(value.Number)) } else { w.Float64(value.Number) } case commontest.StringValue: if variant == commontest.UntypedVariant { // Use our own encoder to encode the string, but then write it with Raw() tw1 := newTokenWriter() _ = tw1.String(value.String) w.Raw(json.RawMessage(tw1.Bytes())) return w.Error() } w.String(value.String) case commontest.ArrayValue: arr := w.Array() for _, e := range value.Array { if err := e(ctx); err != nil { return err } } arr.End() case commontest.ObjectValue: obj := w.Object() for _, p := range value.Object { obj.Name(p.Name) if err := p.Action(ctx); err != nil { return err } } obj.End() } return w.Error() } } go-jsonstream-3.0.0/package_info.go000066400000000000000000000007571430321663200172500ustar00rootroot00000000000000// Package jsonstream provides a fast streaming JSON encoding and decoding mechanism. // // The base package is empty; see the jreader and jwriter subpackages. // // In the default implementation, these packages have no external dependencies. Setting the build // tag "launchdarkly_easyjson" causes them to use https://github.com/mailru/easyjson as the // underlying reader/writer implementation. // // For more information, see: https://github.com/launchdarkly/go-jsonstream package jsonstream