diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..7dc7009 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,52 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "friday" + open-pull-requests-limit: 2 # <- default is 5 + groups: # <- group all github actions updates in a single PR + # 1. development-dependencies are auto-merged + development-dependencies: + patterns: + - '*' + + - package-ecosystem: "gomod" + # We define 4 groups of dependencies to regroup update pull requests: + # - development (e.g. test dependencies) + # - go-openapi updates + # - golang.org (e.g. golang.org/x/... packages) + # - other dependencies (direct or indirect) + # + # * All groups are checked once a week and each produce at most 1 PR. + # * All dependabot PRs are auto-approved + # + # Auto-merging policy, when requirements are met: + # 1. development-dependencies are auto-merged + # 2. golang.org-dependencies are auto-merged + # 3. go-openapi patch updates are auto-merged. Minor/major version updates require a manual merge. + # 4. other dependencies require a manual merge + directory: "/" + schedule: + interval: "weekly" + day: "friday" + open-pull-requests-limit: 4 + groups: + development-dependencies: + patterns: + - "github.com/stretchr/testify" + + golang.org-dependencies: + patterns: + - "golang.org/*" + + go-openapi-dependencies: + patterns: + - "github.com/go-openapi/*" + + other-dependencies: + exclude-patterns: + - "github.com/go-openapi/*" + - "github.com/stretchr/testify" + - "golang.org/*" diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..b4009db --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,43 @@ +name: Dependabot auto-merge +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1 + + - name: Auto-approve all dependabot PRs + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge dependabot PRs for development dependencies + if: contains(steps.metadata.outputs.dependency-group, 'development-dependencies') + run: gh pr merge --auto --rebase "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge dependabot PRs for go-openapi patches + if: contains(steps.metadata.outputs.dependency-group, 'go-openapi-dependencies') && (steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch') + run: gh pr merge --auto --rebase "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge dependabot PRs for golang.org updates + if: contains(steps.metadata.outputs.dependency-group, 'golang.org-dependencies') + run: gh pr merge --auto --rebase "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} + diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 6a230af..e1c413e 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -1,23 +1,58 @@ -name: Go Test +name: go test -on: [push] +on: + push: + tags: + - v* + branches: + - master + + pull_request: jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + check-latest: true + cache: true + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + only-new-issues: true + skip-cache: true test: - - name: Test + name: Unit tests runs-on: ${{ matrix.os }} strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - steps: + go_version: ['oldstable', 'stable' ] - - uses: actions/setup-go@v2 + steps: + - name: Run unit tests + uses: actions/setup-go@v5 with: - go-version: 1.x + go-version: '${{ matrix.go_version }}' + check-latest: true + cache: true + + - uses: actions/checkout@v4 - - uses: actions/checkout@v2 + - run: go test -v -race -coverprofile="coverage-${{ matrix.os }}.${{ matrix.go_version }}.out" -covermode=atomic -coverpkg=$(go list)/... ./... - - run: go test \ No newline at end of file + - name: Upload coverage to codecov + uses: codecov/codecov-action@v4 + with: + files: './coverage-${{ matrix.os }}.${{ matrix.go_version }}.out' + flags: '${{ matrix.go_version }}' + os: '${{ matrix.os }}' + fail_ci_if_error: false + verbose: true diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..22f8d21 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,61 @@ +linters-settings: + govet: + check-shadowing: true + golint: + min-confidence: 0 + gocyclo: + min-complexity: 45 + maligned: + suggest-new: true + dupl: + threshold: 200 + goconst: + min-len: 2 + min-occurrences: 3 + +linters: + enable-all: true + disable: + - maligned + - unparam + - lll + - gochecknoinits + - gochecknoglobals + - funlen + - godox + - gocognit + - whitespace + - wsl + - wrapcheck + - testpackage + - nlreturn + - gomnd + - exhaustivestruct + - goerr113 + - errorlint + - nestif + - godot + - gofumpt + - paralleltest + - tparallel + - thelper + - ifshort + - exhaustruct + - varnamelen + - gci + - depguard + - errchkjson + - inamedparam + - nonamedreturns + - musttag + - ireturn + - forcetypeassert + - cyclop + # deprecated linters + - deadcode + - interfacer + - scopelint + - varcheck + - structcheck + - golint + - nosnakecase diff --git a/README.md b/README.md index 813788a..0108f1d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ -# gojsonpointer [![Build Status](https://travis-ci.org/go-openapi/jsonpointer.svg?branch=master)](https://travis-ci.org/go-openapi/jsonpointer) [![codecov](https://codecov.io/gh/go-openapi/jsonpointer/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/jsonpointer) [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io) +# gojsonpointer [![Build Status](https://github.com/go-openapi/jsonpointer/actions/workflows/go-test.yml/badge.svg)](https://github.com/go-openapi/jsonpointer/actions?query=workflow%3A"go+test") [![codecov](https://codecov.io/gh/go-openapi/jsonpointer/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/jsonpointer) + +[![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io) +[![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/jsonpointer/master/LICENSE) +[![Go Reference](https://pkg.go.dev/badge/github.com/go-openapi/jsonpointer.svg)](https://pkg.go.dev/github.com/go-openapi/jsonpointer) +[![Go Report Card](https://goreportcard.com/badge/github.com/go-openapi/jsonpointer)](https://goreportcard.com/report/github.com/go-openapi/jsonpointer) -[![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/jsonpointer/master/LICENSE) [![GoDoc](https://godoc.org/github.com/go-openapi/jsonpointer?status.svg)](http://godoc.org/github.com/go-openapi/jsonpointer) An implementation of JSON Pointer - Go language ## Status diff --git a/go.mod b/go.mod index ce0ad24..1563e88 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,17 @@ module github.com/go-openapi/jsonpointer require ( - github.com/go-openapi/swag v0.22.3 - github.com/stretchr/testify v1.8.1 + github.com/go-openapi/swag v0.23.0 + github.com/stretchr/testify v1.9.0 ) -go 1.13 +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +go 1.20 diff --git a/go.sum b/go.sum index 10ca578..0922c86 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,24 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 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/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pointer.go b/pointer.go index 7df9853..d970c7c 100644 --- a/pointer.go +++ b/pointer.go @@ -26,6 +26,7 @@ package jsonpointer import ( + "encoding/json" "errors" "fmt" "reflect" @@ -40,6 +41,7 @@ const ( pointerSeparator = `/` invalidStart = `JSON pointer must be empty or start with a "` + pointerSeparator + notFound = `Can't find the pointer in the document` ) var jsonPointableType = reflect.TypeOf(new(JSONPointable)).Elem() @@ -48,13 +50,13 @@ var jsonSetableType = reflect.TypeOf(new(JSONSetable)).Elem() // JSONPointable is an interface for structs to implement when they need to customize the // json pointer process type JSONPointable interface { - JSONLookup(string) (interface{}, error) + JSONLookup(string) (any, error) } // JSONSetable is an interface for structs to implement when they need to customize the // json pointer process type JSONSetable interface { - JSONSet(string, interface{}) error + JSONSet(string, any) error } // New creates a new json pointer for the given string @@ -81,9 +83,7 @@ func (p *Pointer) parse(jsonPointerString string) error { err = errors.New(invalidStart) } else { referenceTokens := strings.Split(jsonPointerString, pointerSeparator) - for _, referenceToken := range referenceTokens[1:] { - p.referenceTokens = append(p.referenceTokens, referenceToken) - } + p.referenceTokens = append(p.referenceTokens, referenceTokens[1:]...) } } @@ -91,38 +91,58 @@ func (p *Pointer) parse(jsonPointerString string) error { } // Get uses the pointer to retrieve a value from a JSON document -func (p *Pointer) Get(document interface{}) (interface{}, reflect.Kind, error) { +func (p *Pointer) Get(document any) (any, reflect.Kind, error) { return p.get(document, swag.DefaultJSONNameProvider) } // Set uses the pointer to set a value from a JSON document -func (p *Pointer) Set(document interface{}, value interface{}) (interface{}, error) { +func (p *Pointer) Set(document any, value any) (any, error) { return document, p.set(document, value, swag.DefaultJSONNameProvider) } // GetForToken gets a value for a json pointer token 1 level deep -func GetForToken(document interface{}, decodedToken string) (interface{}, reflect.Kind, error) { +func GetForToken(document any, decodedToken string) (any, reflect.Kind, error) { return getSingleImpl(document, decodedToken, swag.DefaultJSONNameProvider) } // SetForToken gets a value for a json pointer token 1 level deep -func SetForToken(document interface{}, decodedToken string, value interface{}) (interface{}, error) { +func SetForToken(document any, decodedToken string, value any) (any, error) { return document, setSingleImpl(document, value, decodedToken, swag.DefaultJSONNameProvider) } -func getSingleImpl(node interface{}, decodedToken string, nameProvider *swag.NameProvider) (interface{}, reflect.Kind, error) { +func isNil(input any) bool { + if input == nil { + return true + } + + kind := reflect.TypeOf(input).Kind() + switch kind { //nolint:exhaustive + case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan: + return reflect.ValueOf(input).IsNil() + default: + return false + } +} + +func getSingleImpl(node any, decodedToken string, nameProvider *swag.NameProvider) (any, reflect.Kind, error) { rValue := reflect.Indirect(reflect.ValueOf(node)) kind := rValue.Kind() + if isNil(node) { + return nil, kind, fmt.Errorf("nil value has not field %q", decodedToken) + } - if rValue.Type().Implements(jsonPointableType) { - r, err := node.(JSONPointable).JSONLookup(decodedToken) + switch typed := node.(type) { + case JSONPointable: + r, err := typed.JSONLookup(decodedToken) if err != nil { return nil, kind, err } return r, kind, nil + case *any: // case of a pointer to interface, that is not resolved by reflect.Indirect + return getSingleImpl(*typed, decodedToken, nameProvider) } - switch kind { + switch kind { //nolint:exhaustive case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { @@ -159,7 +179,7 @@ func getSingleImpl(node interface{}, decodedToken string, nameProvider *swag.Nam } -func setSingleImpl(node, data interface{}, decodedToken string, nameProvider *swag.NameProvider) error { +func setSingleImpl(node, data any, decodedToken string, nameProvider *swag.NameProvider) error { rValue := reflect.Indirect(reflect.ValueOf(node)) if ns, ok := node.(JSONSetable); ok { // pointer impl @@ -170,7 +190,7 @@ func setSingleImpl(node, data interface{}, decodedToken string, nameProvider *sw return node.(JSONSetable).JSONSet(decodedToken, data) } - switch rValue.Kind() { + switch rValue.Kind() { //nolint:exhaustive case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { @@ -210,7 +230,7 @@ func setSingleImpl(node, data interface{}, decodedToken string, nameProvider *sw } -func (p *Pointer) get(node interface{}, nameProvider *swag.NameProvider) (interface{}, reflect.Kind, error) { +func (p *Pointer) get(node any, nameProvider *swag.NameProvider) (any, reflect.Kind, error) { if nameProvider == nil { nameProvider = swag.DefaultJSONNameProvider @@ -231,8 +251,7 @@ func (p *Pointer) get(node interface{}, nameProvider *swag.NameProvider) (interf if err != nil { return nil, knd, err } - node, kind = r, knd - + node = r } rValue := reflect.ValueOf(node) @@ -241,11 +260,11 @@ func (p *Pointer) get(node interface{}, nameProvider *swag.NameProvider) (interf return node, kind, nil } -func (p *Pointer) set(node, data interface{}, nameProvider *swag.NameProvider) error { +func (p *Pointer) set(node, data any, nameProvider *swag.NameProvider) error { knd := reflect.ValueOf(node).Kind() if knd != reflect.Ptr && knd != reflect.Struct && knd != reflect.Map && knd != reflect.Slice && knd != reflect.Array { - return fmt.Errorf("only structs, pointers, maps and slices are supported for setting values") + return errors.New("only structs, pointers, maps and slices are supported for setting values") } if nameProvider == nil { @@ -284,7 +303,7 @@ func (p *Pointer) set(node, data interface{}, nameProvider *swag.NameProvider) e continue } - switch kind { + switch kind { //nolint:exhaustive case reflect.Struct: nm, ok := nameProvider.GetGoNameForType(rValue.Type(), decodedToken) if !ok { @@ -363,6 +382,128 @@ func (p *Pointer) String() string { return pointerString } +func (p *Pointer) Offset(document string) (int64, error) { + dec := json.NewDecoder(strings.NewReader(document)) + var offset int64 + for _, ttk := range p.DecodedTokens() { + tk, err := dec.Token() + if err != nil { + return 0, err + } + switch tk := tk.(type) { + case json.Delim: + switch tk { + case '{': + offset, err = offsetSingleObject(dec, ttk) + if err != nil { + return 0, err + } + case '[': + offset, err = offsetSingleArray(dec, ttk) + if err != nil { + return 0, err + } + default: + return 0, fmt.Errorf("invalid token %#v", tk) + } + default: + return 0, fmt.Errorf("invalid token %#v", tk) + } + } + return offset, nil +} + +func offsetSingleObject(dec *json.Decoder, decodedToken string) (int64, error) { + for dec.More() { + offset := dec.InputOffset() + tk, err := dec.Token() + if err != nil { + return 0, err + } + switch tk := tk.(type) { + case json.Delim: + switch tk { + case '{': + if err = drainSingle(dec); err != nil { + return 0, err + } + case '[': + if err = drainSingle(dec); err != nil { + return 0, err + } + } + case string: + if tk == decodedToken { + return offset, nil + } + default: + return 0, fmt.Errorf("invalid token %#v", tk) + } + } + return 0, fmt.Errorf("token reference %q not found", decodedToken) +} + +func offsetSingleArray(dec *json.Decoder, decodedToken string) (int64, error) { + idx, err := strconv.Atoi(decodedToken) + if err != nil { + return 0, fmt.Errorf("token reference %q is not a number: %v", decodedToken, err) + } + var i int + for i = 0; i < idx && dec.More(); i++ { + tk, err := dec.Token() + if err != nil { + return 0, err + } + + if delim, isDelim := tk.(json.Delim); isDelim { + switch delim { + case '{': + if err = drainSingle(dec); err != nil { + return 0, err + } + case '[': + if err = drainSingle(dec); err != nil { + return 0, err + } + } + } + } + + if !dec.More() { + return 0, fmt.Errorf("token reference %q not found", decodedToken) + } + return dec.InputOffset(), nil +} + +// drainSingle drains a single level of object or array. +// The decoder has to guarantee the beginning delim (i.e. '{' or '[') has been consumed. +func drainSingle(dec *json.Decoder) error { + for dec.More() { + tk, err := dec.Token() + if err != nil { + return err + } + if delim, isDelim := tk.(json.Delim); isDelim { + switch delim { + case '{': + if err = drainSingle(dec); err != nil { + return err + } + case '[': + if err = drainSingle(dec); err != nil { + return err + } + } + } + } + + // Consumes the ending delim + if _, err := dec.Token(); err != nil { + return err + } + return nil +} + // Specific JSON pointer encoding here // ~0 => ~ // ~1 => / @@ -377,14 +518,14 @@ const ( // Unescape unescapes a json pointer reference token string to the original representation func Unescape(token string) string { - step1 := strings.Replace(token, encRefTok1, decRefTok1, -1) - step2 := strings.Replace(step1, encRefTok0, decRefTok0, -1) + step1 := strings.ReplaceAll(token, encRefTok1, decRefTok1) + step2 := strings.ReplaceAll(step1, encRefTok0, decRefTok0) return step2 } // Escape escapes a pointer reference token string func Escape(token string) string { - step1 := strings.Replace(token, decRefTok0, encRefTok0, -1) - step2 := strings.Replace(step1, decRefTok1, encRefTok1, -1) + step1 := strings.ReplaceAll(token, decRefTok0, encRefTok0) + step2 := strings.ReplaceAll(step1, decRefTok1, encRefTok1) return step2 } diff --git a/pointer_test.go b/pointer_test.go index 020b19d..6f378e9 100644 --- a/pointer_test.go +++ b/pointer_test.go @@ -32,6 +32,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -52,7 +53,7 @@ const ( }` ) -var testDocumentJSON interface{} +var testDocumentJSON any type testStructJSON struct { Foo []string `json:"foo"` @@ -67,100 +68,135 @@ type testStructJSON struct { } `json:"obj"` } -type aliasedMap map[string]interface{} +type aliasedMap map[string]any var testStructJSONDoc testStructJSON var testStructJSONPtr *testStructJSON func init() { - json.Unmarshal([]byte(TestDocumentString), &testDocumentJSON) - json.Unmarshal([]byte(TestDocumentString), &testStructJSONDoc) + if err := json.Unmarshal([]byte(TestDocumentString), &testDocumentJSON); err != nil { + panic(err) + } + if err := json.Unmarshal([]byte(TestDocumentString), &testStructJSONDoc); err != nil { + panic(err) + } + testStructJSONPtr = &testStructJSONDoc } func TestEscaping(t *testing.T) { - ins := []string{`/`, `/`, `/a~1b`, `/a~1b`, `/c%d`, `/e^f`, `/g|h`, `/i\j`, `/k"l`, `/ `, `/m~0n`} outs := []float64{0, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8} for i := range ins { p, err := New(ins[i]) - if assert.NoError(t, err, "input: %v", ins[i]) { - result, _, err := p.Get(testDocumentJSON) - if assert.NoError(t, err, "input: %v", ins[i]) { - assert.Equal(t, outs[i], result, "input: %v", ins[i]) - } - } + require.NoError(t, err, "input: %v", ins[i]) + result, _, err := p.Get(testDocumentJSON) + + require.NoError(t, err, "input: %v", ins[i]) + assert.InDeltaf(t, outs[i], result, 1e-6, "input: %v", ins[i]) } } func TestFullDocument(t *testing.T) { - - in := `` + const in = `` p, err := New(in) - if err != nil { - t.Errorf("New(%v) error %v", in, err.Error()) - } + require.NoErrorf(t, err, "New(%v) error %v", in, err) result, _, err := p.Get(testDocumentJSON) - if err != nil { - t.Errorf("Get(%v) error %v", in, err.Error()) - } + require.NoErrorf(t, err, "Get(%v) error %v", in, err) - if len(result.(map[string]interface{})) != TestDocumentNBItems { - t.Errorf("Get(%v) = %v, expect full document", in, result) - } + asMap, ok := result.(map[string]any) + require.True(t, ok) + require.Lenf(t, asMap, TestDocumentNBItems, "Get(%v) = %v, expect full document", in, result) result, _, err = p.get(testDocumentJSON, nil) - if err != nil { - t.Errorf("Get(%v) error %v", in, err.Error()) - } + require.NoErrorf(t, err, "Get(%v) error %v", in, err) - if len(result.(map[string]interface{})) != TestDocumentNBItems { - t.Errorf("Get(%v) = %v, expect full document", in, result) - } + asMap, ok = result.(map[string]any) + require.True(t, ok) + require.Lenf(t, asMap, TestDocumentNBItems, "Get(%v) = %v, expect full document", in, result) } func TestDecodedTokens(t *testing.T) { p, err := New("/obj/a~1b") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, []string{"obj", "a/b"}, p.DecodedTokens()) } func TestIsEmpty(t *testing.T) { p, err := New("") - assert.NoError(t, err) + require.NoError(t, err) + assert.True(t, p.IsEmpty()) p, err = New("/obj") - assert.NoError(t, err) + require.NoError(t, err) + assert.False(t, p.IsEmpty()) } func TestGetSingle(t *testing.T) { - in := `/obj` - - _, err := New(in) - assert.NoError(t, err) - result, _, err := GetForToken(testDocumentJSON, "obj") - assert.NoError(t, err) - assert.Len(t, result, TestNodeObjNBItems) - - result, _, err = GetForToken(testStructJSONDoc, "Obj") - assert.Error(t, err) - assert.Nil(t, result) - - result, _, err = GetForToken(testStructJSONDoc, "Obj2") - assert.Error(t, err) - assert.Nil(t, result) + const in = `/obj` + + t.Run("should create a new JSON pointer", func(t *testing.T) { + _, err := New(in) + require.NoError(t, err) + }) + + t.Run(`should find token "obj" in JSON`, func(t *testing.T) { + result, _, err := GetForToken(testDocumentJSON, "obj") + require.NoError(t, err) + assert.Len(t, result, TestNodeObjNBItems) + }) + + t.Run(`should find token "obj" in type alias interface`, func(t *testing.T) { + type alias interface{} + var in alias = testDocumentJSON + result, _, err := GetForToken(in, "obj") + require.NoError(t, err) + assert.Len(t, result, TestNodeObjNBItems) + }) + + t.Run(`should find token "obj" in pointer to interface`, func(t *testing.T) { + in := &testDocumentJSON + result, _, err := GetForToken(in, "obj") + require.NoError(t, err) + assert.Len(t, result, TestNodeObjNBItems) + }) + + t.Run(`should not find token "Obj" in struct`, func(t *testing.T) { + result, _, err := GetForToken(testStructJSONDoc, "Obj") + require.Error(t, err) + assert.Nil(t, result) + }) + + t.Run(`should not find token "Obj2" in struct`, func(t *testing.T) { + result, _, err := GetForToken(testStructJSONDoc, "Obj2") + require.Error(t, err) + assert.Nil(t, result) + }) + + t.Run(`should not find token in nil`, func(t *testing.T) { + result, _, err := GetForToken(nil, "obj") + require.Error(t, err) + assert.Nil(t, result) + }) + + t.Run(`should not find token in nil interface`, func(t *testing.T) { + var in interface{} + result, _, err := GetForToken(in, "obj") + require.Error(t, err) + assert.Nil(t, result) + }) } type pointableImpl struct { a string } -func (p pointableImpl) JSONLookup(token string) (interface{}, error) { +func (p pointableImpl) JSONLookup(token string) (any, error) { if token == "some" { return p.a, nil } @@ -169,7 +205,7 @@ func (p pointableImpl) JSONLookup(token string) (interface{}, error) { type pointableMap map[string]string -func (p pointableMap) JSONLookup(token string) (interface{}, error) { +func (p pointableMap) JSONLookup(token string) (any, error) { if token == "swap" { return p["swapped"], nil } @@ -186,78 +222,78 @@ func TestPointableInterface(t *testing.T) { p := &pointableImpl{"hello"} result, _, err := GetForToken(p, "some") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, p.a, result) result, _, err = GetForToken(p, "something") - assert.Error(t, err) + require.Error(t, err) assert.Nil(t, result) pm := pointableMap{"swapped": "hello", "a": "world"} result, _, err = GetForToken(pm, "swap") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, pm["swapped"], result) result, _, err = GetForToken(pm, "a") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, pm["a"], result) } func TestGetNode(t *testing.T) { - - in := `/obj` + const in = `/obj` p, err := New(in) - assert.NoError(t, err) + require.NoError(t, err) + result, _, err := p.Get(testDocumentJSON) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, result, TestNodeObjNBItems) - result, _, err = p.Get(aliasedMap(testDocumentJSON.(map[string]interface{}))) - assert.NoError(t, err) + result, _, err = p.Get(aliasedMap(testDocumentJSON.(map[string]any))) + require.NoError(t, err) assert.Len(t, result, TestNodeObjNBItems) result, _, err = p.Get(testStructJSONDoc) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, testStructJSONDoc.Obj, result) result, _, err = p.Get(testStructJSONPtr) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, testStructJSONDoc.Obj, result) } func TestArray(t *testing.T) { - ins := []string{`/foo/0`, `/foo/0`, `/foo/1`} outs := []string{"bar", "bar", "baz"} for i := range ins { p, err := New(ins[i]) - assert.NoError(t, err) + require.NoError(t, err) result, _, err := p.Get(testStructJSONDoc) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, outs[i], result) result, _, err = p.Get(testStructJSONPtr) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, outs[i], result) result, _, err = p.Get(testDocumentJSON) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, outs[i], result) } } func TestOtherThings(t *testing.T) { _, err := New("abc") - assert.Error(t, err) + require.Error(t, err) p, err := New("") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "", p.String()) p, err = New("/obj/a") + require.NoError(t, err) assert.Equal(t, "/obj/a", p.String()) s := Escape("m~n") @@ -266,33 +302,33 @@ func TestOtherThings(t *testing.T) { assert.Equal(t, "m~1n", s) p, err = New("/foo/3") - assert.NoError(t, err) + require.NoError(t, err) _, _, err = p.Get(testDocumentJSON) - assert.Error(t, err) + require.Error(t, err) p, err = New("/foo/a") - assert.NoError(t, err) + require.NoError(t, err) _, _, err = p.Get(testDocumentJSON) - assert.Error(t, err) + require.Error(t, err) p, err = New("/notthere") - assert.NoError(t, err) + require.NoError(t, err) _, _, err = p.Get(testDocumentJSON) - assert.Error(t, err) + require.Error(t, err) p, err = New("/invalid") - assert.NoError(t, err) + require.NoError(t, err) _, _, err = p.Get(1234) - assert.Error(t, err) + require.Error(t, err) p, err = New("/foo/1") - assert.NoError(t, err) + require.NoError(t, err) expected := "hello" - bbb := testDocumentJSON.(map[string]interface{})["foo"] - bbb.([]interface{})[1] = "hello" + bbb := testDocumentJSON.(map[string]any)["foo"] + bbb.([]any)[1] = "hello" v, _, err := p.Get(testDocumentJSON) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, expected, v) esc := Escape("a/") @@ -307,34 +343,34 @@ func TestOtherThings(t *testing.T) { } func TestObject(t *testing.T) { - ins := []string{`/obj/a`, `/obj/b`, `/obj/c/0`, `/obj/c/1`, `/obj/c/1`, `/obj/d/1/f/0`} outs := []float64{1, 2, 3, 4, 4, 50} for i := range ins { - p, err := New(ins[i]) - assert.NoError(t, err) + require.NoError(t, err) result, _, err := p.Get(testDocumentJSON) - assert.NoError(t, err) - assert.Equal(t, outs[i], result) + require.NoError(t, err) + assert.InDelta(t, outs[i], result, 1e-6) result, _, err = p.Get(testStructJSONDoc) - assert.NoError(t, err) - assert.EqualValues(t, outs[i], result) + require.NoError(t, err) + assert.InDelta(t, outs[i], result, 1e-6) result, _, err = p.Get(testStructJSONPtr) - assert.NoError(t, err) - assert.EqualValues(t, outs[i], result) + require.NoError(t, err) + assert.InDelta(t, outs[i], result, 1e-6) } } -type setJsonDocEle struct { - B int `json:"b"` - C int `json:"c"` -} -type setJsonDoc struct { +/* + type setJSONDocEle struct { + B int `json:"b"` + C int `json:"c"` + } +*/ +type setJSONDoc struct { A []struct { B int `json:"b"` C int `json:"c"` @@ -371,7 +407,7 @@ func (s *settableDoc) UnmarshalJSON(data []byte) error { } // JSONLookup implements an interface to customize json pointer lookup -func (s settableDoc) JSONLookup(token string) (interface{}, error) { +func (s settableDoc) JSONLookup(token string) (any, error) { switch token { case "a": return &s.Coll, nil @@ -383,7 +419,7 @@ func (s settableDoc) JSONLookup(token string) (interface{}, error) { } // JSONLookup implements an interface to customize json pointer lookup -func (s *settableDoc) JSONSet(token string, data interface{}) error { +func (s *settableDoc) JSONSet(token string, data any) error { switch token { case "a": switch dt := data.(type) { @@ -440,7 +476,7 @@ func (s *settableColl) UnmarshalJSON(data []byte) error { } // JSONLookup implements an interface to customize json pointer lookup -func (s settableColl) JSONLookup(token string) (interface{}, error) { +func (s settableColl) JSONLookup(token string) (any, error) { if tok, err := strconv.Atoi(token); err == nil { return &s.Items[tok], nil } @@ -448,7 +484,7 @@ func (s settableColl) JSONLookup(token string) (interface{}, error) { } // JSONLookup implements an interface to customize json pointer lookup -func (s *settableColl) JSONSet(token string, data interface{}) error { +func (s *settableColl) JSONSet(token string, data any) error { if _, err := strconv.Atoi(token); err == nil { _, err := SetForToken(s.Items, token, data) return err @@ -473,125 +509,203 @@ func (s *settableInt) UnmarshalJSON(data []byte) error { } func TestSetNode(t *testing.T) { + const jsonText = `{"a":[{"b": 1, "c": 2}], "d": 3}` - jsonText := `{"a":[{"b": 1, "c": 2}], "d": 3}` + var jsonDocument any + require.NoError(t, json.Unmarshal([]byte(jsonText), &jsonDocument)) - var jsonDocument interface{} - if assert.NoError(t, json.Unmarshal([]byte(jsonText), &jsonDocument)) { - in := "/a/0/c" + t.Run("with set node c", func(t *testing.T) { + const in = "/a/0/c" p, err := New(in) - if assert.NoError(t, err) { + require.NoError(t, err) - _, err = p.Set(jsonDocument, 999) - assert.NoError(t, err) + _, err = p.Set(jsonDocument, 999) + require.NoError(t, err) - firstNode := jsonDocument.(map[string]interface{}) - assert.Len(t, firstNode, 2) + firstNode, ok := jsonDocument.(map[string]any) + require.True(t, ok) + assert.Len(t, firstNode, 2) - sliceNode := firstNode["a"].([]interface{}) - assert.Len(t, sliceNode, 1) + sliceNode, ok := firstNode["a"].([]any) + require.True(t, ok) + assert.Len(t, sliceNode, 1) - changedNode := sliceNode[0].(map[string]interface{}) - chNodeVI := changedNode["c"] - if assert.IsType(t, 0, chNodeVI) { - changedNodeValue := chNodeVI.(int) - if assert.Equal(t, 999, changedNodeValue) { - assert.Len(t, sliceNode, 1) - } - } - } + changedNode, ok := sliceNode[0].(map[string]any) + require.True(t, ok) + chNodeVI := changedNode["c"] + + require.IsType(t, 0, chNodeVI) + changedNodeValue := chNodeVI.(int) + require.Equal(t, 999, changedNodeValue) + assert.Len(t, sliceNode, 1) + }) + t.Run("with set node 0 with map", func(t *testing.T) { v, err := New("/a/0") - if assert.NoError(t, err) { - _, err = v.Set(jsonDocument, map[string]interface{}{"b": 3, "c": 8}) - if assert.NoError(t, err) { - firstNode := jsonDocument.(map[string]interface{}) - assert.Len(t, firstNode, 2) - - sliceNode := firstNode["a"].([]interface{}) - assert.Len(t, sliceNode, 1) - changedNode := sliceNode[0].(map[string]interface{}) - assert.Equal(t, 3, changedNode["b"]) - assert.Equal(t, 8, changedNode["c"]) - } - } - } + require.NoError(t, err) + + _, err = v.Set(jsonDocument, map[string]any{"b": 3, "c": 8}) + require.NoError(t, err) + + firstNode, ok := jsonDocument.(map[string]any) + require.True(t, ok) + assert.Len(t, firstNode, 2) + + sliceNode, ok := firstNode["a"].([]any) + require.True(t, ok) + assert.Len(t, sliceNode, 1) + + changedNode, ok := sliceNode[0].(map[string]any) + require.True(t, ok) + assert.Equal(t, 3, changedNode["b"]) + assert.Equal(t, 8, changedNode["c"]) + }) + + t.Run("with struct", func(t *testing.T) { + var structDoc setJSONDoc + require.NoError(t, json.Unmarshal([]byte(jsonText), &structDoc)) + + t.Run("with set array node", func(t *testing.T) { + g, err := New("/a") + require.NoError(t, err) - var structDoc setJsonDoc - if assert.NoError(t, json.Unmarshal([]byte(jsonText), &structDoc)) { - g, err := New("/a") - if assert.NoError(t, err) { _, err = g.Set(&structDoc, []struct { B int `json:"b"` C int `json:"c"` }{{B: 4, C: 7}}) + require.NoError(t, err) + assert.Len(t, structDoc.A, 1) + changedNode := structDoc.A[0] + assert.Equal(t, 4, changedNode.B) + assert.Equal(t, 7, changedNode.C) + }) - if assert.NoError(t, err) { - assert.Len(t, structDoc.A, 1) - changedNode := structDoc.A[0] - assert.Equal(t, 4, changedNode.B) - assert.Equal(t, 7, changedNode.C) - } - } + t.Run("with set node 0 with struct", func(t *testing.T) { + v, err := New("/a/0") + require.NoError(t, err) - v, err := New("/a/0") - if assert.NoError(t, err) { _, err = v.Set(structDoc, struct { B int `json:"b"` C int `json:"c"` }{B: 3, C: 8}) + require.NoError(t, err) + assert.Len(t, structDoc.A, 1) + changedNode := structDoc.A[0] + assert.Equal(t, 3, changedNode.B) + assert.Equal(t, 8, changedNode.C) + }) - if assert.NoError(t, err) { - assert.Len(t, structDoc.A, 1) - changedNode := structDoc.A[0] - assert.Equal(t, 3, changedNode.B) - assert.Equal(t, 8, changedNode.C) - } - } + t.Run("with set node c with struct", func(t *testing.T) { + p, err := New("/a/0/c") + require.NoError(t, err) - p, err := New("/a/0/c") - if assert.NoError(t, err) { _, err = p.Set(&structDoc, 999) - assert.NoError(t, err) - if assert.Len(t, structDoc.A, 1) { - assert.Equal(t, 999, structDoc.A[0].C) - } - } - } + require.NoError(t, err) + + require.Len(t, structDoc.A, 1) + assert.Equal(t, 999, structDoc.A[0].C) + }) + }) + + t.Run("with Settable", func(t *testing.T) { + var setDoc settableDoc + require.NoError(t, json.Unmarshal([]byte(jsonText), &setDoc)) + + t.Run("with array node a", func(t *testing.T) { + g, err := New("/a") + require.NoError(t, err) - var setDoc settableDoc - if assert.NoError(t, json.Unmarshal([]byte(jsonText), &setDoc)) { - g, err := New("/a") - if assert.NoError(t, err) { _, err = g.Set(&setDoc, []settableCollItem{{B: 4, C: 7}}) + require.NoError(t, err) + assert.Len(t, setDoc.Coll.Items, 1) + changedNode := setDoc.Coll.Items[0] + assert.Equal(t, 4, changedNode.B) + assert.Equal(t, 7, changedNode.C) + }) - if assert.NoError(t, err) { - assert.Len(t, setDoc.Coll.Items, 1) - changedNode := setDoc.Coll.Items[0] - assert.Equal(t, 4, changedNode.B) - assert.Equal(t, 7, changedNode.C) - } - } + t.Run("with node 0", func(t *testing.T) { + v, err := New("/a/0") + require.NoError(t, err) - v, err := New("/a/0") - if assert.NoError(t, err) { _, err = v.Set(setDoc, settableCollItem{B: 3, C: 8}) + require.NoError(t, err) + assert.Len(t, setDoc.Coll.Items, 1) + changedNode := setDoc.Coll.Items[0] + assert.Equal(t, 3, changedNode.B) + assert.Equal(t, 8, changedNode.C) + }) + + t.Run("with node c", func(t *testing.T) { + p, err := New("/a/0/c") + require.NoError(t, err) + _, err = p.Set(setDoc, 999) + require.NoError(t, err) + require.Len(t, setDoc.Coll.Items, 1) + assert.Equal(t, 999, setDoc.Coll.Items[0].C) + }) + }) +} + +func TestOffset(t *testing.T) { + cases := []struct { + name string + ptr string + input string + offset int64 + hasError bool + }{ + { + name: "object key", + ptr: "/foo/bar", + input: `{"foo": {"bar": 21}}`, + offset: 9, + }, + { + name: "complex object key", + ptr: "/paths/~1p~1{}/get", + input: `{"paths": {"foo": {"bar": 123, "baz": {}}, "/p/{}": {"get": {}}}}`, + offset: 53, + }, + { + name: "array index", + ptr: "/0/1", + input: `[[1,2], [3,4]]`, + offset: 3, + }, + { + name: "mix array index and object key", + ptr: "/0/1/foo/0", + input: `[[1, {"foo": ["a", "b"]}], [3, 4]]`, + offset: 14, + }, + { + name: "nonexist object key", + ptr: "/foo/baz", + input: `{"foo": {"bar": 21}}`, + hasError: true, + }, + { + name: "nonexist array index", + ptr: "/0/2", + input: `[[1,2], [3,4]]`, + hasError: true, + }, + } - if assert.NoError(t, err) { - assert.Len(t, setDoc.Coll.Items, 1) - changedNode := setDoc.Coll.Items[0] - assert.Equal(t, 3, changedNode.B) - assert.Equal(t, 8, changedNode.C) - } - } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + ptr, err := New(tt.ptr) + require.NoError(t, err) - p, err := New("/a/0/c") - if assert.NoError(t, err) { - _, err = p.Set(setDoc, 999) - assert.NoError(t, err) - if assert.Len(t, setDoc.Coll.Items, 1) { - assert.Equal(t, 999, setDoc.Coll.Items[0].C) + offset, err := ptr.Offset(tt.input) + if tt.hasError { + require.Error(t, err) + return } - } + + t.Log(offset, err) + require.NoError(t, err) + assert.Equal(t, tt.offset, offset) + }) } }